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 com.android.cts.verifier.audio; 18 19 import com.android.cts.verifier.PassFailButtons; 20 import com.android.cts.verifier.R; 21 import com.android.cts.verifier.audio.wavelib.*; 22 import com.android.compatibility.common.util.ReportLog; 23 import com.android.compatibility.common.util.ResultType; 24 import com.android.compatibility.common.util.ResultUnit; 25 26 import android.app.AlertDialog; 27 import android.content.Context; 28 import android.media.AudioDeviceCallback; 29 import android.media.AudioDeviceInfo; 30 import android.media.AudioFormat; 31 import android.media.AudioManager; 32 import android.media.AudioTrack; 33 import android.media.AudioRecord; 34 import android.media.MediaRecorder; 35 import android.os.Bundle; 36 import android.os.Handler; 37 import android.os.Message; 38 import android.os.SystemClock; 39 import android.util.Log; 40 import android.view.View; 41 import android.view.View.OnClickListener; 42 import android.widget.Button; 43 import android.widget.TextView; 44 import android.widget.SeekBar; 45 import android.widget.LinearLayout; 46 import android.widget.ProgressBar; 47 48 /** 49 * Tests Audio Device roundtrip latency by using a loopback plug. 50 */ 51 public class AudioFrequencyLineActivity extends AudioFrequencyActivity implements Runnable, 52 AudioRecord.OnRecordPositionUpdateListener { 53 private static final String TAG = "AudioFrequencyLineActivity"; 54 55 static final int TEST_STARTED = 900; 56 static final int TEST_ENDED = 901; 57 static final int TEST_MESSAGE = 902; 58 static final double MIN_ENERGY_BAND_1 = -20.0; 59 static final double MIN_FRACTION_POINTS_IN_BAND = 0.3; 60 61 OnBtnClickListener mBtnClickListener = new OnBtnClickListener(); 62 Context mContext; 63 64 Button mHeadsetPortYes; 65 Button mHeadsetPortNo; 66 67 Button mLoopbackPlugReady; 68 LinearLayout mLinearLayout; 69 Button mTestButton; 70 TextView mResultText; 71 ProgressBar mProgressBar; 72 //recording 73 private boolean mIsRecording = false; 74 private final Object mRecordingLock = new Object(); 75 private AudioRecord mRecorder; 76 private int mMinRecordBufferSizeInSamples = 0; 77 private short[] mAudioShortArray; 78 private short[] mAudioShortArray2; 79 80 private final int mBlockSizeSamples = 1024; 81 private final int mSamplingRate = 48000; 82 private final int mSelectedRecordSource = MediaRecorder.AudioSource.UNPROCESSED; 83 private final int mChannelConfig = AudioFormat.CHANNEL_IN_MONO; 84 private final int mAudioFormat = AudioFormat.ENCODING_PCM_16BIT; 85 private volatile Thread mRecordThread; 86 private boolean mRecordThreadShutdown = false; 87 88 PipeShort mPipe = new PipeShort(65536); 89 SoundPlayerObject mSPlayer; 90 91 private DspBufferComplex mC; 92 private DspBufferDouble mData; 93 94 private DspWindow mWindow; 95 private DspFftServer mFftServer; 96 private VectorAverage mFreqAverageMain = new VectorAverage(); 97 98 private VectorAverage mFreqAverage0 = new VectorAverage(); 99 private VectorAverage mFreqAverage1 = new VectorAverage(); 100 101 private int mCurrentTest = -1; 102 int mBands = 4; 103 AudioBandSpecs[] bandSpecsArray = new AudioBandSpecs[mBands]; 104 105 private class OnBtnClickListener implements OnClickListener { 106 @Override 107 public void onClick(View v) { 108 switch (v.getId()) { 109 case R.id.audio_frequency_line_plug_ready_btn: 110 Log.i(TAG, "audio loopback plug ready"); 111 //enable all the other views. 112 enableLayout(true); 113 setMaxLevel(); 114 testMaxLevel(); 115 break; 116 case R.id.audio_frequency_line_test_btn: 117 Log.i(TAG, "audio loopback test"); 118 startAudioTest(); 119 break; 120 case R.id.audio_general_headset_yes: 121 Log.i(TAG, "User confirms Headset Port existence"); 122 mLoopbackPlugReady.setEnabled(true); 123 recordHeasetPortFound(true); 124 mHeadsetPortYes.setEnabled(false); 125 mHeadsetPortNo.setEnabled(false); 126 break; 127 case R.id.audio_general_headset_no: 128 Log.i(TAG, "User denies Headset Port existence"); 129 recordHeasetPortFound(false); 130 getPassButton().setEnabled(true); 131 mHeadsetPortYes.setEnabled(false); 132 mHeadsetPortNo.setEnabled(false); 133 break; 134 } 135 } 136 } 137 138 @Override 139 protected void onCreate(Bundle savedInstanceState) { 140 super.onCreate(savedInstanceState); 141 setContentView(R.layout.audio_frequency_line_activity); 142 143 mContext = this; 144 145 mHeadsetPortYes = (Button)findViewById(R.id.audio_general_headset_yes); 146 mHeadsetPortYes.setOnClickListener(mBtnClickListener); 147 mHeadsetPortNo = (Button)findViewById(R.id.audio_general_headset_no); 148 mHeadsetPortNo.setOnClickListener(mBtnClickListener); 149 150 mLoopbackPlugReady = (Button)findViewById(R.id.audio_frequency_line_plug_ready_btn); 151 mLoopbackPlugReady.setOnClickListener(mBtnClickListener); 152 mLoopbackPlugReady.setEnabled(false); 153 mLinearLayout = (LinearLayout)findViewById(R.id.audio_frequency_line_layout); 154 mTestButton = (Button)findViewById(R.id.audio_frequency_line_test_btn); 155 mTestButton.setOnClickListener(mBtnClickListener); 156 mResultText = (TextView)findViewById(R.id.audio_frequency_line_results_text); 157 mProgressBar = (ProgressBar)findViewById(R.id.audio_frequency_line_progress_bar); 158 showWait(false); 159 enableLayout(false); //disabled all content 160 161 mSPlayer = new SoundPlayerObject(); 162 mSPlayer.setSoundWithResId(getApplicationContext(), R.raw.stereo_mono_white_noise_48); 163 mSPlayer.setBalance(0.5f); 164 165 //Init FFT stuff 166 mAudioShortArray2 = new short[mBlockSizeSamples*2]; 167 mData = new DspBufferDouble(mBlockSizeSamples); 168 mC = new DspBufferComplex(mBlockSizeSamples); 169 mFftServer = new DspFftServer(mBlockSizeSamples); 170 171 int overlap = mBlockSizeSamples / 2; 172 173 mWindow = new DspWindow(DspWindow.WINDOW_HANNING, mBlockSizeSamples, overlap); 174 175 setPassFailButtonClickListeners(); 176 getPassButton().setEnabled(false); 177 setInfoResources(R.string.audio_frequency_line_test, 178 R.string.audio_frequency_line_info, -1); 179 180 //Init bands 181 bandSpecsArray[0] = new AudioBandSpecs( 182 50, 500, /* frequency start,stop */ 183 4.0, -50, /* start top,bottom value */ 184 4.0, -4.0 /* stop top,bottom value */); 185 186 bandSpecsArray[1] = new AudioBandSpecs( 187 500,4000, /* frequency start,stop */ 188 4.0, -4.0, /* start top,bottom value */ 189 4.0, -4.0 /* stop top,bottom value */); 190 191 bandSpecsArray[2] = new AudioBandSpecs( 192 4000, 12000, /* frequency start,stop */ 193 4.0, -4.0, /* start top,bottom value */ 194 5.0, -5.0 /* stop top,bottom value */); 195 196 bandSpecsArray[3] = new AudioBandSpecs( 197 12000, 20000, /* frequency start,stop */ 198 5.0, -5.0, /* start top,bottom value */ 199 5.0, -30.0 /* stop top,bottom value */); 200 } 201 202 /** 203 * enable test ui elements 204 */ 205 private void enableLayout(boolean enable) { 206 for (int i = 0; i < mLinearLayout.getChildCount(); i++) { 207 View view = mLinearLayout.getChildAt(i); 208 view.setEnabled(enable); 209 } 210 } 211 212 /** 213 * show active progress bar 214 */ 215 private void showWait(boolean show) { 216 if (show) { 217 mProgressBar.setVisibility(View.VISIBLE); 218 } else { 219 mProgressBar.setVisibility(View.INVISIBLE); 220 } 221 } 222 223 /** 224 * Start the loopback audio test 225 */ 226 private void startAudioTest() { 227 if (mTestThread != null && !mTestThread.isAlive()) { 228 mTestThread = null; //kill it. 229 } 230 231 if (mTestThread == null) { 232 Log.v(TAG,"Executing test Thread"); 233 mTestThread = new Thread(mPlayRunnable); 234 getPassButton().setEnabled(false); 235 if (!mSPlayer.isAlive()) 236 mSPlayer.start(); 237 mTestThread.start(); 238 } else { 239 Log.v(TAG,"test Thread already running."); 240 } 241 } 242 243 Thread mTestThread; 244 Runnable mPlayRunnable = new Runnable() { 245 public void run() { 246 Message msg = Message.obtain(); 247 msg.what = TEST_STARTED; 248 mMessageHandler.sendMessage(msg); 249 250 sendMessage("Testing Left Capture"); 251 mCurrentTest = 0; 252 mFreqAverage0.reset(); 253 mSPlayer.setBalance(0.0f); 254 play(); 255 256 sendMessage("Testing Right Capture"); 257 mCurrentTest = 1; 258 mFreqAverage1.reset(); 259 mSPlayer.setBalance(1.0f); 260 play(); 261 262 mCurrentTest = -1; 263 sendMessage("Testing Completed"); 264 265 Message msg2 = Message.obtain(); 266 msg2.what = TEST_ENDED; 267 mMessageHandler.sendMessage(msg2); 268 } 269 270 private void play() { 271 startRecording(); 272 mSPlayer.play(true); 273 274 try { 275 Thread.sleep(2000); 276 } catch (InterruptedException e) { 277 e.printStackTrace(); 278 } 279 280 mSPlayer.play(false); 281 stopRecording(); 282 } 283 284 private void sendMessage(String str) { 285 Message msg = Message.obtain(); 286 msg.what = TEST_MESSAGE; 287 msg.obj = str; 288 mMessageHandler.sendMessage(msg); 289 } 290 }; 291 292 private Handler mMessageHandler = new Handler() { 293 public void handleMessage(Message msg) { 294 super.handleMessage(msg); 295 switch (msg.what) { 296 case TEST_STARTED: 297 showWait(true); 298 getPassButton().setEnabled(false); 299 break; 300 case TEST_ENDED: 301 showWait(false); 302 computeResults(); 303 break; 304 case TEST_MESSAGE: 305 String str = (String)msg.obj; 306 if (str != null) { 307 mResultText.setText(str); 308 } 309 break; 310 default: 311 Log.e(TAG, String.format("Unknown message: %d", msg.what)); 312 } 313 } 314 }; 315 316 private class Results { 317 private String mLabel; 318 public double[] mValuesLog; 319 int[] mPointsPerBand = new int[mBands]; 320 double[] mAverageEnergyPerBand = new double[mBands]; 321 int[] mInBoundPointsPerBand = new int[mBands]; 322 public Results(String label) { 323 mLabel = label; 324 } 325 326 //append results 327 public String toString() { 328 StringBuilder sb = new StringBuilder(); 329 sb.append(String.format("Channel %s\n", mLabel)); 330 sb.append("Level in Band 1 : " + (testLevel() ? "OK" :"Not Optimal") +"\n"); 331 for (int b = 0; b < mBands; b++) { 332 double percent = 0; 333 if (mPointsPerBand[b] > 0) { 334 percent = 100.0 * (double)mInBoundPointsPerBand[b] / mPointsPerBand[b]; 335 } 336 sb.append(String.format( 337 " Band %d: Av. Level: %.1f dB InBand: %d/%d (%.1f%%) %s\n", 338 b, mAverageEnergyPerBand[b], 339 mInBoundPointsPerBand[b], 340 mPointsPerBand[b], 341 percent, 342 (testInBand(b) ? "OK" : "Not Optimal"))); 343 } 344 return sb.toString(); 345 } 346 347 public boolean testLevel() { 348 if (mAverageEnergyPerBand[1] >= MIN_ENERGY_BAND_1) { 349 return true; 350 } 351 return false; 352 } 353 354 public boolean testInBand(int b) { 355 if (b >= 0 && b < mBands && mPointsPerBand[b] > 0) { 356 if ((double)mInBoundPointsPerBand[b] / mPointsPerBand[b] > 357 MIN_FRACTION_POINTS_IN_BAND) 358 return true; 359 } 360 return false; 361 } 362 363 public boolean testAll() { 364 if (!testLevel()) { 365 return false; 366 } 367 for (int b = 0; b < mBands; b++) { 368 if (!testInBand(b)) { 369 return false; 370 } 371 } 372 return true; 373 } 374 } 375 376 /** 377 * compute test results 378 */ 379 private void computeResults() { 380 Results resultsLeft = new Results("Left"); 381 computeResultsForVector(mFreqAverage0, resultsLeft); 382 Results resultsRight = new Results("Right"); 383 computeResultsForVector(mFreqAverage1, resultsRight); 384 if (resultsLeft.testAll() && resultsRight.testAll()) { 385 String strSuccess = getResources().getString(R.string.audio_general_test_passed); 386 appendResultsToScreen(strSuccess); 387 } else { 388 String strFailed = getResources().getString(R.string.audio_general_test_failed); 389 appendResultsToScreen(strFailed + "\n"); 390 String strWarning = getResources().getString(R.string.audio_general_deficiency_found); 391 appendResultsToScreen(strWarning); 392 } 393 getPassButton().setEnabled(true); //Everybody passes! (for now...) 394 } 395 396 private void computeResultsForVector(VectorAverage freqAverage,Results results) { 397 398 int points = freqAverage.getSize(); 399 if (points > 0) { 400 //compute vector in db 401 double[] values = new double[points]; 402 freqAverage.getData(values, false); 403 results.mValuesLog = new double[points]; 404 for (int i = 0; i < points; i++) { 405 results.mValuesLog[i] = 20 * Math.log10(values[i]); 406 } 407 408 int currentBand = 0; 409 for (int i = 0; i < points; i++) { 410 double freq = (double)mSamplingRate * i / (double)mBlockSizeSamples; 411 if (freq > bandSpecsArray[currentBand].mFreqStop) { 412 currentBand++; 413 if (currentBand >= mBands) 414 break; 415 } 416 417 if (freq >= bandSpecsArray[currentBand].mFreqStart) { 418 results.mAverageEnergyPerBand[currentBand] += results.mValuesLog[i]; 419 results.mPointsPerBand[currentBand]++; 420 } 421 } 422 423 for (int b = 0; b < mBands; b++) { 424 if (results.mPointsPerBand[b] > 0) { 425 results.mAverageEnergyPerBand[b] = 426 results.mAverageEnergyPerBand[b] / results.mPointsPerBand[b]; 427 } 428 } 429 430 //set offset relative to band 1 level 431 for (int b = 0; b < mBands; b++) { 432 bandSpecsArray[b].setOffset(results.mAverageEnergyPerBand[1]); 433 } 434 435 //test points in band. 436 currentBand = 0; 437 for (int i = 0; i < points; i++) { 438 double freq = (double)mSamplingRate * i / (double)mBlockSizeSamples; 439 if (freq > bandSpecsArray[currentBand].mFreqStop) { 440 currentBand++; 441 if (currentBand >= mBands) 442 break; 443 } 444 445 if (freq >= bandSpecsArray[currentBand].mFreqStart) { 446 double value = results.mValuesLog[i]; 447 if (bandSpecsArray[currentBand].isInBounds(freq, value)) { 448 results.mInBoundPointsPerBand[currentBand]++; 449 } 450 } 451 } 452 453 appendResultsToScreen(results.toString()); 454 //store results 455 recordTestResults(results); 456 } else { 457 appendResultsToScreen("Failed testing channel " + results.mLabel); 458 } 459 } 460 461 //append results 462 private void appendResultsToScreen(String str) { 463 String currentText = mResultText.getText().toString(); 464 mResultText.setText(currentText + "\n" + str); 465 } 466 467 /** 468 * Store test results in log 469 */ 470 private void recordTestResults(Results results) { 471 String channelLabel = "channel_" + results.mLabel; 472 473 for (int b = 0; b < mBands; b++) { 474 String bandLabel = String.format(channelLabel + "_%d", b); 475 getReportLog().addValue( 476 bandLabel + "_Level", 477 results.mAverageEnergyPerBand[b], 478 ResultType.HIGHER_BETTER, 479 ResultUnit.NONE); 480 481 getReportLog().addValue( 482 bandLabel + "_pointsinbound", 483 results.mInBoundPointsPerBand[b], 484 ResultType.HIGHER_BETTER, 485 ResultUnit.COUNT); 486 487 getReportLog().addValue( 488 bandLabel + "_pointstotal", 489 results.mPointsPerBand[b], 490 ResultType.NEUTRAL, 491 ResultUnit.COUNT); 492 } 493 494 getReportLog().addValues(channelLabel + "_magnitudeSpectrumLog", 495 results.mValuesLog, 496 ResultType.NEUTRAL, 497 ResultUnit.NONE); 498 499 Log.v(TAG, "Results Recorded"); 500 } 501 502 private void recordHeasetPortFound(boolean found) { 503 getReportLog().addValue( 504 "User Reported Headset Port", 505 found ? 1.0 : 0, 506 ResultType.NEUTRAL, 507 ResultUnit.NONE); 508 } 509 510 private void startRecording() { 511 synchronized (mRecordingLock) { 512 mIsRecording = true; 513 } 514 515 boolean successful = initRecord(); 516 if (successful) { 517 startRecordingForReal(); 518 } else { 519 Log.v(TAG, "Recorder initialization error."); 520 synchronized (mRecordingLock) { 521 mIsRecording = false; 522 } 523 } 524 } 525 526 private void startRecordingForReal() { 527 // start streaming 528 if (mRecordThread == null) { 529 mRecordThread = new Thread(AudioFrequencyLineActivity.this); 530 mRecordThread.setName("FrequencyAnalyzerThread"); 531 mRecordThreadShutdown = false; 532 } 533 if (!mRecordThread.isAlive()) { 534 mRecordThread.start(); 535 } 536 537 mPipe.flush(); 538 539 long startTime = SystemClock.uptimeMillis(); 540 mRecorder.startRecording(); 541 if (mRecorder.getRecordingState() != AudioRecord.RECORDSTATE_RECORDING) { 542 stopRecording(); 543 return; 544 } 545 Log.v(TAG, "Start time: " + (long) (SystemClock.uptimeMillis() - startTime) + " ms"); 546 } 547 548 private void stopRecording() { 549 synchronized (mRecordingLock) { 550 stopRecordingForReal(); 551 mIsRecording = false; 552 } 553 } 554 555 private void stopRecordingForReal() { 556 557 // stop streaming 558 Thread zeThread = mRecordThread; 559 mRecordThread = null; 560 mRecordThreadShutdown = true; 561 if (zeThread != null) { 562 zeThread.interrupt(); 563 try { 564 zeThread.join(); 565 } catch(InterruptedException e) { 566 Log.v(TAG,"Error shutting down recording thread " + e); 567 //we don't really care about this error, just logging it. 568 } 569 } 570 // release recording resources 571 if (mRecorder != null) { 572 mRecorder.stop(); 573 mRecorder.release(); 574 mRecorder = null; 575 } 576 } 577 578 private boolean initRecord() { 579 int minRecordBuffSizeInBytes = AudioRecord.getMinBufferSize(mSamplingRate, 580 mChannelConfig, mAudioFormat); 581 Log.v(TAG,"FrequencyAnalyzer: min buff size = " + minRecordBuffSizeInBytes + " bytes"); 582 if (minRecordBuffSizeInBytes <= 0) { 583 return false; 584 } 585 586 mMinRecordBufferSizeInSamples = minRecordBuffSizeInBytes / 2; 587 // allocate the byte array to read the audio data 588 589 mAudioShortArray = new short[mMinRecordBufferSizeInSamples]; 590 591 Log.v(TAG, "Initiating record:"); 592 Log.v(TAG, " using source " + mSelectedRecordSource); 593 Log.v(TAG, " at " + mSamplingRate + "Hz"); 594 595 try { 596 mRecorder = new AudioRecord(mSelectedRecordSource, mSamplingRate, 597 mChannelConfig, mAudioFormat, 2 * minRecordBuffSizeInBytes); 598 } catch (IllegalArgumentException e) { 599 Log.v(TAG, "Error: " + e.toString()); 600 return false; 601 } 602 if (mRecorder.getState() != AudioRecord.STATE_INITIALIZED) { 603 mRecorder.release(); 604 mRecorder = null; 605 Log.v(TAG, "Error: mRecorder not initialized"); 606 return false; 607 } 608 mRecorder.setRecordPositionUpdateListener(this); 609 mRecorder.setPositionNotificationPeriod(mBlockSizeSamples / 2); 610 return true; 611 } 612 613 // --------------------------------------------------------- 614 // Implementation of AudioRecord.OnPeriodicNotificationListener 615 // -------------------- 616 public void onPeriodicNotification(AudioRecord recorder) { 617 int samplesAvailable = mPipe.availableToRead(); 618 int samplesNeeded = mBlockSizeSamples; 619 if (samplesAvailable >= samplesNeeded) { 620 mPipe.read(mAudioShortArray2, 0, samplesNeeded); 621 622 //compute stuff. 623 double maxval = Math.pow(2, 15); 624 int clipcount = 0; 625 double cliplevel = (maxval-10) / maxval; 626 double sum = 0; 627 double maxabs = 0; 628 int i; 629 int index = 0; 630 631 for (i = 0; i < samplesNeeded; i++) { 632 double value = mAudioShortArray2[i] / maxval; 633 double valueabs = Math.abs(value); 634 635 if (valueabs > maxabs) { 636 maxabs = valueabs; 637 } 638 639 if (valueabs > cliplevel) { 640 clipcount++; 641 } 642 643 sum += value * value; 644 //fft stuff 645 if (index < mBlockSizeSamples) { 646 mData.mData[index] = value; 647 } 648 index++; 649 } 650 651 //for the current frame, compute FFT and send to the viewer. 652 653 //apply window and pack as complex for now. 654 DspBufferMath.mult(mData, mData, mWindow.mBuffer); 655 DspBufferMath.set(mC, mData); 656 mFftServer.fft(mC, 1); 657 658 double[] halfMagnitude = new double[mBlockSizeSamples / 2]; 659 for (i = 0; i < mBlockSizeSamples / 2; i++) { 660 halfMagnitude[i] = Math.sqrt(mC.mReal[i] * mC.mReal[i] + mC.mImag[i] * mC.mImag[i]); 661 } 662 663 mFreqAverageMain.setData(halfMagnitude, false); //average all of them! 664 665 switch(mCurrentTest) { 666 case 0: 667 mFreqAverage0.setData(halfMagnitude, false); 668 break; 669 case 1: 670 mFreqAverage1.setData(halfMagnitude, false); 671 break; 672 } 673 } 674 } 675 676 public void onMarkerReached(AudioRecord track) { 677 } 678 679 // --------------------------------------------------------- 680 // Implementation of Runnable for the audio recording + playback 681 // -------------------- 682 public void run() { 683 int nSamplesRead = 0; 684 685 Thread thisThread = Thread.currentThread(); 686 while (mRecordThread == thisThread && !mRecordThreadShutdown) { 687 // read from native recorder 688 nSamplesRead = mRecorder.read(mAudioShortArray, 0, mMinRecordBufferSizeInSamples); 689 if (nSamplesRead > 0) { 690 mPipe.write(mAudioShortArray, 0, nSamplesRead); 691 } 692 } 693 } 694 } 695