1 /* 2 * Copyright (C) 2014 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.AudioDeviceInfo; 21 import android.media.AudioFormat; 22 import android.media.AudioManager; 23 import android.media.AudioTrack; 24 import android.media.MediaRecorder; 25 import android.os.Build; 26 import android.util.Log; 27 import android.os.Handler; 28 import android.os.Message; 29 30 /** 31 * A thread/audio track based audio synth. 32 */ 33 34 public class LoopbackAudioThread extends Thread { 35 private static final String TAG = "LoopbackAudioThread"; 36 37 private static final int THREAD_SLEEP_DURATION_MS = 1; 38 39 // for latency test 40 static final int LOOPBACK_AUDIO_THREAD_MESSAGE_LATENCY_REC_STARTED = 991; 41 static final int LOOPBACK_AUDIO_THREAD_MESSAGE_LATENCY_REC_ERROR = 992; 42 static final int LOOPBACK_AUDIO_THREAD_MESSAGE_LATENCY_REC_COMPLETE = 993; 43 static final int LOOPBACK_AUDIO_THREAD_MESSAGE_LATENCY_REC_STOP = 994; 44 45 // for buffer test 46 static final int LOOPBACK_AUDIO_THREAD_MESSAGE_BUFFER_REC_STARTED = 996; 47 static final int LOOPBACK_AUDIO_THREAD_MESSAGE_BUFFER_REC_ERROR = 997; 48 static final int LOOPBACK_AUDIO_THREAD_MESSAGE_BUFFER_REC_COMPLETE = 998; 49 static final int LOOPBACK_AUDIO_THREAD_MESSAGE_BUFFER_REC_STOP = 999; 50 51 public boolean mIsRunning = false; 52 public AudioTrack mAudioTrack; 53 public int mSessionId; 54 private Thread mRecorderThread; 55 private RecorderRunnable mRecorderRunnable; 56 57 private final int mSamplingRate; 58 private final int mChannelIndex; 59 private final int mChannelConfigIn = AudioFormat.CHANNEL_IN_MONO; 60 private final int mAudioFormat = AudioFormat.ENCODING_PCM_16BIT; 61 private int mMinPlayerBufferSizeInBytes = 0; 62 private int mMinRecorderBuffSizeInBytes = 0; 63 private int mMinPlayerBufferSizeSamples = 0; 64 private final int mMicSource; 65 private final int mChannelConfigOut = AudioFormat.CHANNEL_OUT_MONO; 66 private boolean mIsPlaying = false; 67 private boolean mIsRequestStop = false; 68 private Handler mMessageHandler; 69 // This is the pipe that connects the player and the recorder in latency test. 70 private PipeShort mLatencyTestPipe = new PipeShort(Constant.MAX_SHORTS); 71 72 // for buffer test 73 private BufferPeriod mRecorderBufferPeriod; // used to collect recorder's buffer period 74 private BufferPeriod mPlayerBufferPeriod; // used to collect player's buffer period 75 private int mTestType; // latency test or buffer test 76 private int mBufferTestDurationInSeconds; // Duration of actual buffer test 77 private Context mContext; 78 private int mBufferTestWavePlotDurationInSeconds; 79 private final CaptureHolder mCaptureHolder; 80 private boolean mIsAdjustingSoundLevel = true; // only used in buffer test 81 82 83 public LoopbackAudioThread(int samplingRate, int playerBufferInBytes, int recorderBufferInBytes, 84 int micSource, BufferPeriod recorderBufferPeriod, 85 BufferPeriod playerBufferPeriod, int testType, 86 int bufferTestDurationInSeconds, 87 int bufferTestWavePlotDurationInSeconds, Context context, 88 int channelIndex, CaptureHolder captureHolder) { 89 mSamplingRate = samplingRate; 90 mMinPlayerBufferSizeInBytes = playerBufferInBytes; 91 mMinRecorderBuffSizeInBytes = recorderBufferInBytes; 92 mMicSource = micSource; 93 mRecorderBufferPeriod = recorderBufferPeriod; 94 mPlayerBufferPeriod = playerBufferPeriod; 95 mTestType = testType; 96 mBufferTestDurationInSeconds = bufferTestDurationInSeconds; 97 mBufferTestWavePlotDurationInSeconds = bufferTestWavePlotDurationInSeconds; 98 mContext = context; 99 mChannelIndex = channelIndex; 100 mCaptureHolder = captureHolder; 101 102 setName("Loopback_LoopbackAudio"); 103 } 104 105 106 public void run() { 107 setPriority(Thread.MAX_PRIORITY); 108 109 if (mMinPlayerBufferSizeInBytes <= 0) { 110 mMinPlayerBufferSizeInBytes = AudioTrack.getMinBufferSize(mSamplingRate, 111 mChannelConfigOut, mAudioFormat); 112 113 log("Player: computed min buff size = " + mMinPlayerBufferSizeInBytes + " bytes"); 114 } else { 115 log("Player: using min buff size = " + mMinPlayerBufferSizeInBytes + " bytes"); 116 } 117 118 mMinPlayerBufferSizeSamples = mMinPlayerBufferSizeInBytes / Constant.BYTES_PER_FRAME; 119 short[] audioShortArrayOut = new short[mMinPlayerBufferSizeSamples]; 120 121 // we may want to adjust this to different multiplication of mMinPlayerBufferSizeSamples 122 int audioTrackWriteDataSize = mMinPlayerBufferSizeSamples; 123 124 // used for buffer test only 125 final double frequency1 = Constant.PRIME_FREQUENCY_1; 126 final double frequency2 = Constant.PRIME_FREQUENCY_2; // not actually used 127 short[] bufferTestTone = new short[audioTrackWriteDataSize]; // used by AudioTrack.write() 128 ToneGeneration toneGeneration = new SineWaveTone(mSamplingRate, frequency1); 129 130 mRecorderRunnable = new RecorderRunnable(mLatencyTestPipe, mSamplingRate, mChannelConfigIn, 131 mAudioFormat, mMinRecorderBuffSizeInBytes, MediaRecorder.AudioSource.MIC, this, 132 mRecorderBufferPeriod, mTestType, frequency1, frequency2, 133 mBufferTestWavePlotDurationInSeconds, mContext, mChannelIndex, mCaptureHolder); 134 mRecorderRunnable.setBufferTestDurationInSeconds(mBufferTestDurationInSeconds); 135 mRecorderThread = new Thread(mRecorderRunnable); 136 mRecorderThread.setName("Loopback_RecorderRunnable"); 137 138 // both player and recorder run at max priority 139 mRecorderThread.setPriority(Thread.MAX_PRIORITY); 140 mRecorderThread.start(); 141 142 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 143 mAudioTrack = new AudioTrack.Builder() 144 .setAudioFormat((mChannelIndex < 0 ? 145 new AudioFormat.Builder().setChannelMask(AudioFormat.CHANNEL_OUT_MONO) : 146 new AudioFormat.Builder().setChannelIndexMask(1 << mChannelIndex)) 147 .setSampleRate(mSamplingRate) 148 .setEncoding(mAudioFormat) 149 .build()) 150 .setBufferSizeInBytes(mMinPlayerBufferSizeInBytes) 151 .setTransferMode(AudioTrack.MODE_STREAM) 152 .build(); 153 } else { 154 mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, 155 mSamplingRate, 156 mChannelConfigOut, 157 mAudioFormat, 158 mMinPlayerBufferSizeInBytes, 159 AudioTrack.MODE_STREAM /* FIXME runtime test for API level 9, 160 mSessionId */); 161 } 162 163 if (mRecorderRunnable != null && mAudioTrack.getState() == AudioTrack.STATE_INITIALIZED) { 164 mIsPlaying = false; 165 mIsRunning = true; 166 167 while (mIsRunning && mRecorderThread.isAlive()) { 168 if (mIsPlaying) { 169 switch (mTestType) { 170 case Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_LATENCY: 171 // read from the pipe and plays it out 172 int samplesAvailable = mLatencyTestPipe.availableToRead(); 173 if (samplesAvailable > 0) { 174 int samplesOfInterest = Math.min(samplesAvailable, 175 mMinPlayerBufferSizeSamples); 176 177 int samplesRead = mLatencyTestPipe.read(audioShortArrayOut, 0, 178 samplesOfInterest); 179 mAudioTrack.write(audioShortArrayOut, 0, samplesRead); 180 mPlayerBufferPeriod.collectBufferPeriod(); 181 } 182 break; 183 case Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_BUFFER_PERIOD: 184 // don't collect buffer period when we are still adjusting the sound level 185 if (mIsAdjustingSoundLevel) { 186 toneGeneration.generateTone(bufferTestTone, bufferTestTone.length); 187 mAudioTrack.write(bufferTestTone, 0, audioTrackWriteDataSize); 188 } else { 189 mPlayerBufferPeriod.collectBufferPeriod(); 190 toneGeneration.generateTone(bufferTestTone, bufferTestTone.length); 191 mAudioTrack.write(bufferTestTone, 0, audioTrackWriteDataSize); 192 } 193 break; 194 } 195 } else { 196 // wait for a bit to allow AudioTrack to start playing 197 if (mIsRunning) { 198 try { 199 sleep(THREAD_SLEEP_DURATION_MS); 200 } catch (InterruptedException e) { 201 e.printStackTrace(); 202 } 203 } 204 } 205 } 206 endTest(); 207 208 } else { 209 log("Loopback Audio Thread couldn't run!"); 210 mAudioTrack.release(); 211 mAudioTrack = null; 212 if (mMessageHandler != null) { 213 Message msg = Message.obtain(); 214 switch (mTestType) { 215 case Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_LATENCY: 216 msg.what = LOOPBACK_AUDIO_THREAD_MESSAGE_LATENCY_REC_ERROR; 217 break; 218 case Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_BUFFER_PERIOD: 219 msg.what = LOOPBACK_AUDIO_THREAD_MESSAGE_BUFFER_REC_ERROR; 220 break; 221 } 222 223 mMessageHandler.sendMessage(msg); 224 } 225 226 } 227 } 228 229 230 public void setMessageHandler(Handler messageHandler) { 231 mMessageHandler = messageHandler; 232 } 233 234 235 public void setIsAdjustingSoundLevel(boolean isAdjustingSoundLevel) { 236 mIsAdjustingSoundLevel = isAdjustingSoundLevel; 237 } 238 239 240 public void runTest() { 241 if (mIsRunning) { 242 // start test 243 if (mAudioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) { 244 log("...run test, but still playing..."); 245 endTest(); 246 } else { 247 // start playing 248 mIsPlaying = true; 249 mAudioTrack.play(); 250 boolean status = mRecorderRunnable.startRecording(); 251 252 log("Started capture test"); 253 if (mMessageHandler != null) { 254 Message msg = Message.obtain(); 255 msg.what = LOOPBACK_AUDIO_THREAD_MESSAGE_LATENCY_REC_STARTED; 256 if (!status) { 257 msg.what = LOOPBACK_AUDIO_THREAD_MESSAGE_LATENCY_REC_ERROR; 258 } 259 260 mMessageHandler.sendMessage(msg); 261 } 262 } 263 } 264 } 265 266 267 public void runBufferTest() { 268 if (mIsRunning) { 269 // start test 270 if (mAudioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) { 271 log("...run test, but still playing..."); 272 endTest(); 273 } else { 274 // start playing 275 mIsPlaying = true; 276 mAudioTrack.play(); 277 boolean status = mRecorderRunnable.startBufferRecording(); 278 log(" Started capture test"); 279 if (mMessageHandler != null) { 280 Message msg = Message.obtain(); 281 msg.what = LOOPBACK_AUDIO_THREAD_MESSAGE_BUFFER_REC_STARTED; 282 283 if (!status) { 284 msg.what = LOOPBACK_AUDIO_THREAD_MESSAGE_BUFFER_REC_ERROR; 285 } 286 287 mMessageHandler.sendMessage(msg); 288 } 289 } 290 } 291 } 292 293 294 /** Clean some things up before sending out a message to LoopbackActivity. */ 295 public void endTest() { 296 switch (mTestType) { 297 case Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_LATENCY: 298 log("--Ending latency test--"); 299 break; 300 case Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_BUFFER_PERIOD: 301 log("--Ending buffer test--"); 302 break; 303 } 304 305 mIsPlaying = false; 306 mAudioTrack.pause(); 307 mLatencyTestPipe.flush(); 308 mAudioTrack.flush(); 309 310 if (mMessageHandler != null) { 311 Message msg = Message.obtain(); 312 if (mIsRequestStop) { 313 switch (mTestType) { 314 case Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_LATENCY: 315 msg.what = LOOPBACK_AUDIO_THREAD_MESSAGE_LATENCY_REC_STOP; 316 break; 317 case Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_BUFFER_PERIOD: 318 msg.what = LOOPBACK_AUDIO_THREAD_MESSAGE_BUFFER_REC_STOP; 319 break; 320 } 321 } else { 322 switch (mTestType) { 323 case Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_LATENCY: 324 msg.what = LOOPBACK_AUDIO_THREAD_MESSAGE_LATENCY_REC_COMPLETE; 325 break; 326 case Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_BUFFER_PERIOD: 327 msg.what = LOOPBACK_AUDIO_THREAD_MESSAGE_BUFFER_REC_COMPLETE; 328 break; 329 } 330 } 331 332 mMessageHandler.sendMessage(msg); 333 } 334 } 335 336 337 /** 338 * This is called only when the user requests to stop the test through 339 * pressing a button in the LoopbackActivity. 340 */ 341 public void requestStopTest() throws InterruptedException { 342 mIsRequestStop = true; 343 mRecorderRunnable.requestStop(); 344 } 345 346 347 /** Release mAudioTrack and mRecorderThread. */ 348 public void finish() throws InterruptedException { 349 mIsRunning = false; 350 351 final AudioTrack at = mAudioTrack; 352 if (at != null) { 353 at.release(); 354 mAudioTrack = null; 355 } 356 357 Thread zeThread = mRecorderThread; 358 mRecorderThread = null; 359 if (zeThread != null) { 360 zeThread.interrupt(); 361 zeThread.join(Constant.JOIN_WAIT_TIME_MS); 362 } 363 } 364 365 366 private static void log(String msg) { 367 Log.v(TAG, msg); 368 } 369 370 371 public double[] getWaveData() { 372 return mRecorderRunnable.getWaveData(); 373 } 374 375 376 public int[] getAllGlitches() { 377 return mRecorderRunnable.getAllGlitches(); 378 } 379 380 381 public boolean getGlitchingIntervalTooLong() { 382 return mRecorderRunnable.getGlitchingIntervalTooLong(); 383 } 384 385 386 public int getFFTSamplingSize() { 387 return mRecorderRunnable.getFFTSamplingSize(); 388 } 389 390 391 public int getFFTOverlapSamples() { 392 return mRecorderRunnable.getFFTOverlapSamples(); 393 } 394 395 396 int getDurationInSeconds() { 397 return mBufferTestDurationInSeconds; 398 } 399 400 } 401