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.content.Context; 20 import android.media.AudioAttributes; 21 import android.media.AudioFormat; 22 import android.media.AudioManager; 23 import android.media.AudioTrack; 24 import android.media.MediaCodec; 25 import android.media.MediaCodecList; 26 import android.media.MediaExtractor; 27 import android.media.MediaFormat; 28 import android.media.MediaPlayer; 29 import android.net.Uri; 30 import android.util.Log; 31 32 import com.android.cts.verifier.audio.wavelib.PipeShort; 33 34 import java.io.IOException; 35 import java.nio.ByteBuffer; 36 37 public class SoundPlayerObject implements Runnable, AudioTrack.OnPlaybackPositionUpdateListener { 38 private static final String LOGTAG = "SoundPlayerObject"; 39 40 private static final int TIME_OUT_US = 5000; 41 private static final int DEFAULT_BLOCK_SIZE = 4096; 42 43 private MediaPlayer mMediaPlayer; 44 private boolean isInitialized = false; 45 private boolean isRunning = false; 46 47 private int bufferSize = 65536; 48 private PipeShort mDecoderPipe = new PipeShort(bufferSize); 49 public PipeShort mPipe = new PipeShort(65536); 50 private short[] mAudioShortArray; 51 52 private AudioTrack mAudioTrack; 53 private int mSamplingRate = 48000; 54 private int mChannelConfigOut = AudioFormat.CHANNEL_OUT_MONO; 55 private int mAudioFormat = AudioFormat.ENCODING_PCM_16BIT; 56 private int mMinPlayBufferSizeInBytes = 0; 57 private int mMinBufferSizeInSamples = 0; 58 59 private int mStreamType = AudioManager.STREAM_MUSIC; 60 private int mResId = -1; 61 private final boolean mUseMediaPlayer; //true: MediaPlayer, false: AudioTrack 62 private float mBalance = 0.5f; //0 left, 1 right 63 64 private Object mLock = new Object(); 65 private MediaCodec mCodec = null; 66 private MediaExtractor mExtractor = null; 67 private int mInputAudioFormat; 68 private int mInputSampleRate; 69 private int mInputChannelCount; 70 71 private boolean mLooping = true; 72 private Context mContext = null; 73 74 private final int mBlockSizeSamples; 75 76 private Thread mPlaybackThread; 77 SoundPlayerObject()78 public SoundPlayerObject() { 79 mUseMediaPlayer = true; 80 mBlockSizeSamples = DEFAULT_BLOCK_SIZE; 81 } 82 SoundPlayerObject(boolean useMediaPlayer, int blockSize)83 public SoundPlayerObject(boolean useMediaPlayer, int blockSize) { 84 mUseMediaPlayer = useMediaPlayer; 85 mBlockSizeSamples = blockSize; 86 } 87 getCurrentResId()88 public int getCurrentResId() { 89 return mResId; 90 } 91 getStreamType()92 public int getStreamType () { 93 return mStreamType; 94 } 95 getChannelCount()96 public int getChannelCount() { 97 return mInputChannelCount; 98 } 99 run()100 public void run() { 101 isRunning = true; 102 int decodeRounds = 0; 103 MediaCodec.BufferInfo buffInfo = new MediaCodec.BufferInfo(); 104 105 while (isRunning) { 106 if (Thread.interrupted()) { 107 log("got thread interrupted!"); 108 isRunning = false; 109 return; 110 } 111 112 if (mUseMediaPlayer) { 113 log("run . using media player"); 114 try { 115 Thread.sleep(10); 116 } catch (InterruptedException e) { 117 e.printStackTrace(); 118 } 119 } else { 120 if (isInitialized && !outputEosSignalled) { 121 122 synchronized (mLock) { 123 124 int bytesPerSample = getBytesPerSample(mInputAudioFormat); 125 126 int valuesAvailable = mDecoderPipe.availableToRead(); 127 if (valuesAvailable > 0 && mAudioTrack != null) { 128 int valuesOfInterest = valuesAvailable; 129 if (mMinBufferSizeInSamples < valuesOfInterest) { 130 valuesOfInterest = mMinBufferSizeInSamples; 131 } 132 mDecoderPipe.read(mAudioShortArray, 0, valuesOfInterest); 133 //inject into output. 134 mAudioTrack.write(mAudioShortArray, 0, valuesOfInterest); 135 136 //delay 137 int delayMs = (int)(1000.0f * valuesOfInterest / 138 (float)(mInputSampleRate * bytesPerSample * 139 mInputChannelCount)); 140 delayMs = Math.max(2, delayMs); 141 142 try { 143 Thread.sleep(delayMs); 144 } catch (InterruptedException e) { 145 e.printStackTrace(); 146 } 147 } else { 148 //read another block if possible 149 150 decodeRounds++; 151 if (!inputEosSignalled) { 152 final int inputBufIndex = mCodec.dequeueInputBuffer(TIME_OUT_US); 153 if (inputBufIndex >= 0) { 154 ByteBuffer encodedBuf = mCodec.getInputBuffer(inputBufIndex); 155 final int sampleSize = 156 mExtractor.readSampleData(encodedBuf, 0 /* offset */); 157 long presentationTimeUs = 0; 158 if (sampleSize < 0) { 159 inputEosSignalled = true; 160 log("[v] input EOS at decode round " + decodeRounds); 161 162 } else { 163 presentationTimeUs = mExtractor.getSampleTime(); 164 } 165 mCodec.queueInputBuffer(inputBufIndex, 0/*offset*/, 166 inputEosSignalled ? 0 : sampleSize, presentationTimeUs, 167 (inputEosSignalled && !mLooping) ? 168 MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0); 169 if (!inputEosSignalled) { 170 mExtractor.advance(); 171 } 172 173 if (inputEosSignalled && mLooping) { 174 log("looping is enabled. Rewinding"); 175 mExtractor.seekTo(0, MediaExtractor.SEEK_TO_PREVIOUS_SYNC); 176 inputEosSignalled = false; 177 } 178 } else { 179 log("[v] no input buffer available at decode round " 180 + decodeRounds); 181 } 182 } //!inputEosSignalled 183 184 final int outputRes = mCodec.dequeueOutputBuffer(buffInfo, TIME_OUT_US); 185 186 if (outputRes >= 0) { 187 if (buffInfo.size > 0) { 188 final int outputBufIndex = outputRes; 189 final ByteBuffer decodedBuf = 190 mCodec.getOutputBuffer(outputBufIndex); 191 192 short sValue = 0; //scaled to 16 bits 193 int index = 0; 194 for (int i = 0; i < buffInfo.size && index < 195 mAudioShortArray.length; i += bytesPerSample) { 196 switch (mInputAudioFormat) { 197 case AudioFormat.ENCODING_PCM_FLOAT: 198 sValue = (short) (decodedBuf.getFloat(i) * 32768.0); 199 break; 200 case AudioFormat.ENCODING_PCM_16BIT: 201 sValue = (short) decodedBuf.getShort(i); 202 break; 203 case AudioFormat.ENCODING_PCM_8BIT: 204 sValue = (short) ((decodedBuf.getChar(i) - 128) * 205 128); 206 break; 207 } 208 mAudioShortArray[index] = sValue; 209 index++; 210 } 211 mDecoderPipe.write(mAudioShortArray, 0, index); 212 mPipe.write(mAudioShortArray, 0, index); 213 214 mCodec.getOutputBuffer(outputBufIndex).position(0); 215 mCodec.releaseOutputBuffer(outputBufIndex, 216 false/*render to surface*/); 217 if ((buffInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 218 0) { 219 outputEosSignalled = true; 220 } 221 } 222 } else if (outputRes == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { 223 log("[w] INFO_OUTPUT_FORMAT_CHANGED at decode round " + 224 decodeRounds); 225 decodeRounds = 0; 226 } else if (outputRes == MediaCodec.INFO_TRY_AGAIN_LATER) { 227 log("[w] INFO_TRY_AGAIN_LATER at decode round " + decodeRounds); 228 if (!mLooping) { 229 outputEosSignalled = true; //quit! 230 } 231 } 232 } 233 } 234 235 if (outputEosSignalled) { 236 log ("note: outputEosSignalled"); 237 } 238 } 239 else { 240 try { 241 Thread.sleep(10); 242 } catch (InterruptedException e) { 243 e.printStackTrace(); 244 } 245 } 246 } 247 } 248 log("done running thread"); 249 } 250 setBalance(float balance)251 public void setBalance(float balance) { 252 mBalance = balance; 253 if (mUseMediaPlayer) { 254 if (mMediaPlayer != null) { 255 float left = Math.min(2.0f * (1.0f - mBalance), 1.0f); 256 float right = Math.min(2.0f * mBalance, 1.0f); 257 mMediaPlayer.setVolume(left, right); 258 log(String.format("Setting balance to %f", mBalance)); 259 } 260 } 261 } 262 setStreamType(int streamType)263 public void setStreamType(int streamType) { 264 mStreamType = streamType; 265 } 266 rewind()267 public void rewind() { 268 if (mUseMediaPlayer) { 269 if (mMediaPlayer != null) { 270 mMediaPlayer.seekTo(0); 271 } 272 } 273 } 274 275 boolean inputEosSignalled = false; 276 boolean outputEosSignalled = false; 277 setSoundWithResId(Context context, int resId)278 public void setSoundWithResId(Context context, int resId) { 279 280 setSoundWithResId(context, resId, true); 281 } 282 setSoundWithResId(Context context, int resId, boolean looping)283 public void setSoundWithResId(Context context, int resId, boolean looping) { 284 log("setSoundWithResId " + resId + ", looping: " + looping); 285 mLooping = looping; 286 mContext = context; 287 if (mContext == null) { 288 log("Context can't be null"); 289 return; 290 } 291 292 boolean playing = isPlaying(); 293 if (playing) { 294 play(false); 295 } 296 //release player 297 releasePlayer(); 298 isInitialized = false; 299 300 log("loading uri: " + resId); 301 mResId = resId; 302 Uri uri = Uri.parse("android.resource://com.android.cts.verifier/" + resId); 303 if (mUseMediaPlayer) { 304 mMediaPlayer = new MediaPlayer(); 305 try { 306 log("opening resource with stream type: " + mStreamType); 307 mMediaPlayer.setAudioStreamType(mStreamType); 308 mMediaPlayer.setDataSource(mContext.getApplicationContext(), 309 uri); 310 mMediaPlayer.prepare(); 311 } catch (IOException e) { 312 e.printStackTrace(); 313 log("Error: " + e.toString()); 314 } 315 mMediaPlayer.setLooping(mLooping); 316 setBalance(mBalance); 317 isInitialized = true; 318 319 } else { 320 synchronized (mLock) { 321 //TODO: encapsulate MediaPlayer and AudioTrack related code into separate classes 322 // with common interface. Simplify locking code. 323 mDecoderPipe.flush(); 324 mPipe.flush(); 325 326 if (mCodec != null) 327 mCodec = null; 328 try { 329 mExtractor = new MediaExtractor(); 330 mExtractor.setDataSource(mContext.getApplicationContext(), uri, null); 331 final int trackCount = mExtractor.getTrackCount(); 332 333 // log("Track count: " + trackCount); 334 // find first audio track 335 MediaFormat format = null; 336 String mime = null; 337 for (int i = 0; i < trackCount; i++) { 338 format = mExtractor.getTrackFormat(i); 339 mime = format.getString(MediaFormat.KEY_MIME); 340 if (mime.startsWith("audio/")) { 341 mExtractor.selectTrack(i); 342 // log("found track " + i + " with MIME = " + mime); 343 break; 344 } 345 } 346 if (format == null) { 347 log("found 0 audio tracks in " + uri); 348 } 349 MediaCodecList mclist = new MediaCodecList(MediaCodecList.ALL_CODECS); 350 351 String myDecoderName = mclist.findDecoderForFormat(format); 352 // log("my decoder name = " + myDecoderName); 353 354 mCodec = MediaCodec.createByCodecName(myDecoderName); 355 356 // log("[ok] about to configure codec with " + format); 357 mCodec.configure(format, null /* surface */, null /* crypto */, 0 /* flags */); 358 359 // prepare decoding 360 mCodec.start(); 361 362 inputEosSignalled = false; 363 outputEosSignalled = false; 364 365 MediaFormat outputFormat = format; 366 367 printAudioFormat(outputFormat); 368 369 //int sampleRate 370 mInputSampleRate = getMediaFormatInteger(outputFormat, 371 MediaFormat.KEY_SAMPLE_RATE, 48000); 372 //int channelCount 373 mInputChannelCount = getMediaFormatInteger(outputFormat, 374 MediaFormat.KEY_CHANNEL_COUNT, 1); 375 376 mInputAudioFormat = getMediaFormatInteger(outputFormat, 377 MediaFormat.KEY_PCM_ENCODING, 378 AudioFormat.ENCODING_PCM_16BIT); 379 380 int channelMask = channelMaskFromCount(mInputChannelCount); 381 int buffSize = AudioTrack.getMinBufferSize(mInputSampleRate, channelMask, 382 mInputAudioFormat); 383 384 AudioAttributes.Builder aab = new AudioAttributes.Builder() 385 .setLegacyStreamType(mStreamType); 386 387 AudioFormat.Builder afb = new AudioFormat.Builder() 388 .setEncoding(mInputAudioFormat) 389 .setSampleRate(mInputSampleRate) 390 .setChannelMask(channelMask); 391 392 AudioTrack.Builder atb = new AudioTrack.Builder() 393 .setAudioAttributes(aab.build()) 394 .setAudioFormat(afb.build()) 395 .setBufferSizeInBytes(buffSize) 396 .setTransferMode(AudioTrack.MODE_STREAM); 397 398 mAudioTrack = atb.build(); 399 400 mMinPlayBufferSizeInBytes = AudioTrack.getMinBufferSize(mInputSampleRate, 401 mChannelConfigOut, mInputAudioFormat); 402 403 mMinBufferSizeInSamples = mMinPlayBufferSizeInBytes / 2; 404 mAudioShortArray = new short[mMinBufferSizeInSamples * 100]; 405 mAudioTrack.setPlaybackPositionUpdateListener(this); 406 mAudioTrack.setPositionNotificationPeriod(mBlockSizeSamples); 407 isInitialized = true; 408 } catch (IOException e) { 409 e.printStackTrace(); 410 log("Error creating codec or extractor: " + e.toString()); 411 } 412 } 413 } //mLock 414 415 // log("done preparing media player"); 416 if (playing) 417 play(true); //start playing if it was playing before 418 } 419 isPlaying()420 public boolean isPlaying() { 421 boolean result = false; 422 if (mUseMediaPlayer) { 423 if (mMediaPlayer != null) { 424 result = mMediaPlayer.isPlaying(); 425 } 426 } else { 427 synchronized (mLock) { 428 if (mAudioTrack != null) { 429 result = mAudioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING; 430 } 431 } 432 } 433 return result; 434 } 435 isAlive()436 public boolean isAlive() { 437 if (mUseMediaPlayer) { 438 return true; 439 } 440 441 synchronized (mLock) { 442 if (mPlaybackThread != null) { 443 return mPlaybackThread.isAlive(); 444 } 445 } 446 return false; 447 } 448 start()449 public void start() { 450 if (!mUseMediaPlayer) { 451 synchronized (mLock) { 452 if (mPlaybackThread == null) { 453 mPlaybackThread = new Thread(this); 454 mPlaybackThread.setName("playbackThread"); 455 log("Created playback thread " + mPlaybackThread); 456 } 457 458 if (!mPlaybackThread.isAlive()) { 459 mPlaybackThread.start(); 460 mPlaybackThread.setPriority(Thread.MAX_PRIORITY); 461 log("Started playback thread " + mPlaybackThread); 462 } 463 } 464 } 465 } 466 play(boolean play)467 public void play(boolean play) { 468 if (mUseMediaPlayer) { 469 if (mMediaPlayer != null) { 470 if (play) { 471 mMediaPlayer.start(); 472 } else { 473 mMediaPlayer.pause(); 474 } 475 } 476 } else { 477 synchronized (mLock) { 478 log(" called Play : " + play); 479 if (mAudioTrack != null && isInitialized) { 480 if (play) { 481 log("Play"); 482 mDecoderPipe.flush(); 483 mPipe.flush(); 484 mAudioTrack.play(); 485 } else { 486 log("pause"); 487 mAudioTrack.pause(); 488 isRunning = false; 489 } 490 } 491 } 492 start(); 493 } 494 } 495 finish()496 public void finish() { 497 play(false); 498 releasePlayer(); 499 } 500 releasePlayer()501 private void releasePlayer() { 502 if (mMediaPlayer != null) { 503 mMediaPlayer.stop(); 504 mMediaPlayer.release(); 505 mMediaPlayer = null; 506 } 507 508 isRunning = false; 509 synchronized (mLock) { 510 if (mAudioTrack != null) { 511 mAudioTrack.stop(); 512 mAudioTrack.setPlaybackPositionUpdateListener(null); 513 mAudioTrack.release(); 514 mAudioTrack = null; 515 } 516 } 517 518 log("Deleting playback thread " + mPlaybackThread); 519 Thread zeThread = mPlaybackThread; 520 mPlaybackThread = null; 521 522 if (zeThread != null) { 523 log("terminating zeThread..."); 524 zeThread.interrupt(); 525 try { 526 log("zeThread join..."); 527 zeThread.join(); 528 } catch (InterruptedException e) { 529 log("issue deleting playback thread " + e.toString()); 530 zeThread.interrupt(); 531 } 532 } 533 isInitialized = false; 534 log("Done deleting thread"); 535 536 } 537 538 /* 539 Misc 540 */ log(String msg)541 private static void log(String msg) { 542 Log.v(LOGTAG, msg); 543 } 544 getMediaFormatInteger(MediaFormat mf, String name, int defaultValue)545 private final int getMediaFormatInteger(MediaFormat mf, String name, int defaultValue) { 546 try { 547 return mf.getInteger(name); 548 } catch (NullPointerException e) { 549 log("Warning: MediaFormat " + name + 550 " field does not exist. Using default " + defaultValue); /* no such field */ 551 } catch (ClassCastException e) { 552 log("Warning: MediaFormat " + name + 553 " field unexpected type"); /* field of different type */ 554 } 555 return defaultValue; 556 } 557 getBytesPerSample(int audioFormat)558 public static int getBytesPerSample(int audioFormat) { 559 switch(audioFormat) { 560 case AudioFormat.ENCODING_PCM_16BIT: 561 return 2; 562 case AudioFormat.ENCODING_PCM_8BIT: 563 return 1; 564 case AudioFormat.ENCODING_PCM_FLOAT: 565 return 4; 566 } 567 return 0; 568 } 569 printAudioFormat(MediaFormat format)570 private void printAudioFormat(MediaFormat format) { 571 try { 572 log("channel mask = " + format.getInteger(MediaFormat.KEY_CHANNEL_MASK)); 573 } catch (NullPointerException npe) { 574 log("channel mask unknown"); 575 } 576 try { 577 log("channel count = " + format.getInteger(MediaFormat.KEY_CHANNEL_COUNT)); 578 } catch (NullPointerException npe) { 579 log("channel count unknown"); 580 } 581 try { 582 log("sample rate = " + format.getInteger(MediaFormat.KEY_SAMPLE_RATE)); 583 } catch (NullPointerException npe) { 584 log("sample rate unknown"); 585 } 586 try { 587 log("sample format = " + format.getInteger(MediaFormat.KEY_PCM_ENCODING)); 588 } catch (NullPointerException npe) { 589 log("sample format unknown"); 590 } 591 } 592 channelMaskFromCount(int channelCount)593 public static int channelMaskFromCount(int channelCount) { 594 switch(channelCount) { 595 case 1: 596 return AudioFormat.CHANNEL_OUT_MONO; 597 case 2: 598 return AudioFormat.CHANNEL_OUT_STEREO; 599 case 3: 600 return AudioFormat.CHANNEL_OUT_STEREO | AudioFormat.CHANNEL_OUT_FRONT_CENTER; 601 case 4: 602 return AudioFormat.CHANNEL_OUT_FRONT_LEFT | 603 AudioFormat.CHANNEL_OUT_FRONT_RIGHT | AudioFormat.CHANNEL_OUT_BACK_LEFT | 604 AudioFormat.CHANNEL_OUT_BACK_RIGHT; 605 case 5: 606 return AudioFormat.CHANNEL_OUT_FRONT_LEFT | 607 AudioFormat.CHANNEL_OUT_FRONT_RIGHT | AudioFormat.CHANNEL_OUT_BACK_LEFT | 608 AudioFormat.CHANNEL_OUT_BACK_RIGHT 609 | AudioFormat.CHANNEL_OUT_FRONT_CENTER; 610 case 6: 611 return AudioFormat.CHANNEL_OUT_5POINT1; 612 default: 613 return 0; 614 } 615 } 616 periodicNotification(AudioTrack track)617 public void periodicNotification(AudioTrack track) { 618 } 619 markerReached(AudioTrack track)620 public void markerReached(AudioTrack track) { 621 } 622 623 @Override onMarkerReached(AudioTrack track)624 public void onMarkerReached(AudioTrack track) { 625 markerReached(track); 626 } 627 628 @Override onPeriodicNotification(AudioTrack track)629 public void onPeriodicNotification(AudioTrack track) { 630 periodicNotification(track); 631 } 632 } 633