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