1 /* 2 * Copyright (C) 2012 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.audiotest; 18 19 import android.app.Activity; 20 import android.media.AudioFormat; 21 import android.media.AudioManager; 22 import android.media.AudioRecord; 23 import android.media.MediaRecorder.AudioSource; 24 import android.media.AudioTrack; 25 import android.os.Build; 26 import android.os.Looper; 27 import android.util.Log; 28 29 import java.io.IOException; 30 import java.io.InputStream; 31 import java.io.OutputStream; 32 import java.lang.Thread; 33 import java.net.ServerSocket; 34 import java.net.Socket; 35 import java.net.SocketTimeoutException; 36 import java.nio.ByteBuffer; 37 import java.util.HashMap; 38 import java.util.concurrent.locks.ReentrantLock; 39 40 41 public class AudioProtocol implements AudioTrack.OnPlaybackPositionUpdateListener { 42 private static final String TAG = "AudioProtocol"; 43 private static final int PORT_NUMBER = 15001; 44 45 private Thread mThread = new Thread(new ProtocolServer()); 46 private boolean mExitRequested = false; 47 48 private static final int PROTOCOL_HEADER_SIZE = 8; // id + payload length 49 private static final int MAX_NON_DATA_PAYLOAD_SIZE = 20; 50 private static final int PROTOCOL_SIMPLE_REPLY_SIZE = 12; 51 private static final int PROTOCOL_OK = 0; 52 private static final int PROTOCOL_ERROR_WRONG_PARAM = 1; 53 private static final int PROTOCOL_ERROR_GENERIC = 2; 54 55 private static final int CMD_DOWNLOAD = 0x12340001; 56 private static final int CMD_START_PLAYBACK = 0x12340002; 57 private static final int CMD_STOP_PLAYBACK = 0x12340003; 58 private static final int CMD_START_RECORDING = 0x12340004; 59 private static final int CMD_STOP_RECORDING = 0x12340005; 60 private static final int CMD_GET_DEVICE_INFO = 0x12340006; 61 62 private ByteBuffer mHeaderBuffer = ByteBuffer.allocate(PROTOCOL_HEADER_SIZE); 63 private ByteBuffer mDataBuffer = ByteBuffer.allocate(MAX_NON_DATA_PAYLOAD_SIZE); 64 private ByteBuffer mReplyBuffer = ByteBuffer.allocate(PROTOCOL_SIMPLE_REPLY_SIZE); 65 66 // all socket access (accept / read) set this timeout to check exit periodically. 67 private static final int SOCKET_ACCESS_TIMEOUT = 2000; 68 private Socket mClient = null; 69 private InputStream mInput = null; 70 private OutputStream mOutput = null; 71 // lock to use to write to socket, I/O streams, and also change socket (create, destroy) 72 private ReentrantLock mClientLock = new ReentrantLock(); 73 74 private AudioRecord mRecord = null; 75 private LoopThread mRecordThread = null; 76 private AudioTrack mPlayback = null; 77 private LoopThread mPlaybackThread = null; 78 // store recording length 79 private int mRecordingLength = 0; 80 81 // map for playback data 82 private HashMap<Integer, ByteBuffer> mDataMap = new HashMap<Integer, ByteBuffer>(); 83 start()84 public boolean start() { 85 Log.d(TAG, "start"); 86 mExitRequested = false; 87 mThread.start(); 88 //Log.d(TAG, "started"); 89 return true; 90 } 91 stop()92 public void stop() throws InterruptedException { 93 Log.d(TAG, "stop"); 94 mExitRequested = true; 95 try { 96 mClientLock.lock(); 97 if (mClient != null) { 98 // wake up from socket read 99 mClient.shutdownInput(); 100 } 101 }catch (IOException e) { 102 // ignore 103 } finally { 104 mClientLock.unlock(); 105 } 106 mThread.interrupt(); // this does not bail out from socket in android 107 mThread.join(); 108 reset(); 109 Log.d(TAG, "stopped"); 110 } 111 112 @Override onMarkerReached(AudioTrack track)113 public void onMarkerReached(AudioTrack track) { 114 Log.d(TAG, "playback completed"); 115 track.stop(); 116 track.flush(); 117 track.release(); 118 mPlaybackThread.quitLoop(); 119 mPlaybackThread = null; 120 try { 121 sendSimpleReplyHeader(CMD_START_PLAYBACK, PROTOCOL_OK); 122 } catch (IOException e) { 123 // maybe socket already closed. don't do anything 124 Log.e(TAG, "ignore exception", e); 125 } 126 } 127 128 @Override onPeriodicNotification(AudioTrack arg0)129 public void onPeriodicNotification(AudioTrack arg0) { 130 Log.d(TAG, "track periodic notification"); 131 // TODO Auto-generated method stub 132 } 133 134 /** 135 * Read given amount of data to the buffer 136 * @param in 137 * @param buffer 138 * @param len length to read 139 * @return true if header read successfully, false if exit requested 140 * @throws IOException 141 * @throws ExitRequest 142 */ read(InputStream in, ByteBuffer buffer, int len)143 private void read(InputStream in, ByteBuffer buffer, int len) throws IOException, ExitRequest { 144 buffer.clear(); 145 int totalRead = 0; 146 while (totalRead < len) { 147 int readNow = in.read(buffer.array(), totalRead, len - totalRead); 148 if (readNow < 0) { // end-of-stream, error 149 Log.e(TAG, "read returned " + readNow); 150 throw new IOException(); 151 } 152 totalRead += readNow; 153 if(mExitRequested) { 154 throw new ExitRequest(); 155 } 156 } 157 } 158 159 private class ProtocolError extends Exception { ProtocolError(String message)160 public ProtocolError(String message) { 161 super(message); 162 } 163 } 164 165 private class ExitRequest extends Exception { ExitRequest()166 public ExitRequest() { 167 super(); 168 } 169 } 170 assertProtocol(boolean cond, String message)171 private void assertProtocol(boolean cond, String message) throws ProtocolError { 172 if (!cond) { 173 throw new ProtocolError(message); 174 } 175 } 176 reset()177 private void reset() { 178 // lock only when it is not already locked by this thread 179 if (mClientLock.getHoldCount() == 0) { 180 mClientLock.lock(); 181 } 182 if (mClient != null) { 183 try { 184 mClient.close(); 185 } catch (IOException e) { 186 // ignore 187 } 188 mClient = null; 189 } 190 mInput = null; 191 mOutput = null; 192 while (mClientLock.getHoldCount() > 0) { 193 mClientLock.unlock(); 194 } 195 if (mRecord != null) { 196 if (mRecord.getState() != AudioRecord.STATE_UNINITIALIZED) { 197 mRecord.stop(); 198 } 199 mRecord.release(); 200 mRecord = null; 201 } 202 if (mRecordThread != null) { 203 mRecordThread.quitLoop(); 204 mRecordThread = null; 205 } 206 if (mPlayback != null) { 207 if (mPlayback.getState() != AudioTrack.STATE_UNINITIALIZED) { 208 mPlayback.stop(); 209 mPlayback.flush(); 210 } 211 mPlayback.release(); 212 mPlayback = null; 213 } 214 if (mPlaybackThread != null) { 215 mPlaybackThread.quitLoop(); 216 mPlaybackThread = null; 217 } 218 mDataMap.clear(); 219 } 220 handleDownload(int len)221 private void handleDownload(int len) throws IOException, ExitRequest { 222 read(mInput, mDataBuffer, 4); // only for id 223 Integer id = new Integer(mDataBuffer.getInt(0)); 224 int dataLength = len - 4; 225 ByteBuffer data = ByteBuffer.allocate(dataLength); 226 read(mInput, data, dataLength); 227 mDataMap.put(id, data); 228 Log.d(TAG, "downloaded data id " + id + " len " + dataLength); 229 sendSimpleReplyHeader(CMD_DOWNLOAD, PROTOCOL_OK); 230 } 231 handleStartPlayback(int len)232 private void handleStartPlayback(int len) throws ProtocolError, IOException, ExitRequest { 233 // this error is too critical, so do not even send reply 234 assertProtocol(len == 20, "wrong payload len"); 235 read(mInput, mDataBuffer, len); 236 final Integer id = new Integer(mDataBuffer.getInt(0)); 237 final int samplingRate = mDataBuffer.getInt(1 * 4); 238 final boolean stereo = ((mDataBuffer.getInt(2 * 4) & 0x80000000) != 0); 239 final int mode = mDataBuffer.getInt(2 * 4) & 0x7fffffff; 240 final int volume = mDataBuffer.getInt(3 * 4); 241 final int repeat = mDataBuffer.getInt(4 * 4); 242 try { 243 final ByteBuffer data = mDataMap.get(id); 244 if (data == null) { 245 throw new ProtocolError("wrong id"); 246 } 247 if (samplingRate != 44100) { 248 throw new ProtocolError("wrong rate"); 249 } 250 //FIXME in MODE_STATIC, setNotificationMarkerPosition does not work with full length 251 mPlaybackThread = new LoopThread(new Runnable() { 252 253 @Override 254 public void run() { 255 if (mPlayback != null) { 256 mPlayback.release(); 257 mPlayback = null; 258 } 259 // STREAM_VOICE_CALL activates different speaker. 260 // use MUSIC mode to activate the louder speaker. 261 int type = AudioManager.STREAM_MUSIC; 262 int bufferSize = AudioTrack.getMinBufferSize(samplingRate, 263 stereo ? AudioFormat.CHANNEL_OUT_STEREO : AudioFormat.CHANNEL_OUT_MONO, 264 AudioFormat.ENCODING_PCM_16BIT); 265 bufferSize = bufferSize * 4; 266 if (bufferSize < 256 * 1024) { 267 bufferSize = 256 * 1024; 268 } 269 if (bufferSize > data.capacity()) { 270 bufferSize = data.capacity(); 271 } 272 mPlayback = new AudioTrack(type, samplingRate, 273 stereo ? AudioFormat.CHANNEL_OUT_STEREO : AudioFormat.CHANNEL_OUT_MONO, 274 AudioFormat.ENCODING_PCM_16BIT, bufferSize, 275 AudioTrack.MODE_STREAM); 276 float minVolume = mPlayback.getMinVolume(); 277 float maxVolume = mPlayback.getMaxVolume(); 278 float newVolume = (maxVolume - minVolume) * volume / 100 + minVolume; 279 mPlayback.setStereoVolume(newVolume, newVolume); 280 Log.d(TAG, "setting volume " + newVolume + " max " + maxVolume + 281 " min " + minVolume + " received " + volume); 282 int dataWritten = 0; 283 int dataToWrite = (bufferSize < data.capacity())? bufferSize : data.capacity(); 284 mPlayback.write(data.array(), 0, dataToWrite); 285 dataWritten = dataToWrite; 286 mPlayback.setPlaybackPositionUpdateListener(AudioProtocol.this); 287 288 int endMarker = data.capacity()/(stereo ? 4 : 2); 289 int res = mPlayback.setNotificationMarkerPosition(endMarker); 290 Log.d(TAG, "start playback id " + id + " len " + data.capacity() + 291 " set.. res " + res + " stereo? " + stereo + " mode " + mode + 292 " end " + endMarker); 293 mPlayback.play(); 294 while (dataWritten < data.capacity()) { 295 int dataLeft = data.capacity() - dataWritten; 296 dataToWrite = (bufferSize < dataLeft)? bufferSize : dataLeft; 297 if (mPlayback == null) { // stopped 298 return; 299 } 300 mPlayback.write(data.array(), dataWritten, dataToWrite); 301 dataWritten += dataToWrite; 302 } 303 } 304 }); 305 mPlaybackThread.start(); 306 // send reply when play is completed 307 } catch (ProtocolError e) { 308 sendSimpleReplyHeader(CMD_START_PLAYBACK, PROTOCOL_ERROR_WRONG_PARAM); 309 Log.e(TAG, "wrong param", e); 310 } 311 } 312 handleStopPlayback(int len)313 private void handleStopPlayback(int len) throws ProtocolError, IOException { 314 Log.d(TAG, "stopPlayback"); 315 assertProtocol(len == 0, "wrong payload len"); 316 if (mPlayback != null) { 317 Log.d(TAG, "release AudioTrack"); 318 mPlayback.stop(); 319 mPlayback.flush(); 320 mPlayback.release(); 321 mPlayback = null; 322 } 323 if (mPlaybackThread != null) { 324 mPlaybackThread.quitLoop(); 325 mPlaybackThread = null; 326 } 327 sendSimpleReplyHeader(CMD_STOP_PLAYBACK, PROTOCOL_OK); 328 } 329 handleStartRecording(int len)330 private void handleStartRecording(int len) throws ProtocolError, IOException, ExitRequest { 331 assertProtocol(len == 16, "wrong payload len"); 332 read(mInput, mDataBuffer, len); 333 final int samplingRate = mDataBuffer.getInt(0); 334 final boolean stereo = ((mDataBuffer.getInt(1 * 4) & 0x80000000) != 0); 335 final int mode = mDataBuffer.getInt(1 * 4) & 0x7fffffff; 336 final int volume = mDataBuffer.getInt(2 * 4); 337 final int samples = mDataBuffer.getInt(3 * 4); 338 try { 339 if (samplingRate != 44100) { 340 throw new ProtocolError("wrong rate"); 341 } 342 if (stereo) { 343 throw new ProtocolError("mono only"); 344 } 345 //TODO volume ? 346 mRecordingLength = samples * 2; 347 mRecordThread = new LoopThread(new Runnable() { 348 349 @Override 350 public void run() { 351 int minBufferSize = AudioRecord.getMinBufferSize(samplingRate, 352 AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT); 353 int type = (mode == 0) ? AudioSource.VOICE_RECOGNITION : AudioSource.DEFAULT; 354 mRecord = new AudioRecord(type, samplingRate, 355 AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, 356 (minBufferSize > mRecordingLength) ? minBufferSize : mRecordingLength); 357 358 mRecord.startRecording(); 359 Log.d(TAG, "recording started " + " samples " + samples + " mode " + mode + 360 " recording state " + mRecord.getRecordingState() + " len " + 361 mRecordingLength); 362 try { 363 boolean recordingOk = true; 364 byte[] data = new byte[mRecordingLength]; 365 int totalRead = 0; 366 while (totalRead < mRecordingLength) { 367 int lenRead = mRecord.read(data, 0, (mRecordingLength - totalRead)); 368 if (lenRead < 0) { 369 Log.e(TAG, "reading recording failed with error code " + lenRead); 370 recordingOk = false; 371 break; 372 } else if (lenRead == 0) { 373 Log.w(TAG, "zero read"); 374 } 375 totalRead += lenRead; 376 } 377 Log.d(TAG, "reading recording completed"); 378 sendReplyWithData( 379 CMD_START_RECORDING, 380 recordingOk ? PROTOCOL_OK : PROTOCOL_ERROR_GENERIC, 381 recordingOk ? mRecordingLength : 0, 382 recordingOk ? data : null); 383 } catch (IOException e) { 384 // maybe socket already closed. don't do anything 385 Log.e(TAG, "ignore exception", e); 386 } finally { 387 mRecord.stop(); 388 mRecord.release(); 389 mRecord = null; 390 } 391 } 392 }); 393 mRecordThread.start(); 394 } catch (ProtocolError e) { 395 sendSimpleReplyHeader(CMD_START_RECORDING, PROTOCOL_ERROR_WRONG_PARAM); 396 Log.e(TAG, "wrong param", e); 397 } 398 } 399 handleStopRecording(int len)400 private void handleStopRecording(int len) throws ProtocolError, IOException { 401 Log.d(TAG, "stop recording"); 402 assertProtocol(len == 0, "wrong payload len"); 403 if (mRecord != null) { 404 mRecord.stop(); 405 mRecord.release(); 406 mRecord = null; 407 } 408 if (mRecordThread != null) { 409 mRecordThread.quitLoop(); 410 mRecordThread = null; 411 } 412 sendSimpleReplyHeader(CMD_STOP_RECORDING, PROTOCOL_OK); 413 } 414 415 private static final String BUILD_INFO_TAG = "build-info"; 416 appendAttrib(StringBuilder builder, String name, String value)417 private void appendAttrib(StringBuilder builder, String name, String value) { 418 builder.append(" " + name + "=\"" + value + "\""); 419 } 420 handleGetDeviceInfo(int len)421 private void handleGetDeviceInfo(int len) throws ProtocolError, IOException{ 422 Log.d(TAG, "getDeviceInfo"); 423 assertProtocol(len == 0, "wrong payload len"); 424 StringBuilder builder = new StringBuilder(); 425 builder.append("<build-info"); 426 appendAttrib(builder, "board", Build.BOARD); 427 appendAttrib(builder, "brand", Build.BRAND); 428 appendAttrib(builder, "device", Build.DEVICE); 429 appendAttrib(builder, "display", Build.DISPLAY); 430 appendAttrib(builder, "fingerprint", Build.FINGERPRINT); 431 appendAttrib(builder, "id", Build.ID); 432 appendAttrib(builder, "model", Build.MODEL); 433 appendAttrib(builder, "product", Build.PRODUCT); 434 appendAttrib(builder, "release", Build.VERSION.RELEASE); 435 appendAttrib(builder, "sdk", Integer.toString(Build.VERSION.SDK_INT)); 436 builder.append(" />"); 437 byte[] data = builder.toString().getBytes(); 438 439 sendReplyWithData(CMD_GET_DEVICE_INFO, PROTOCOL_OK, data.length, data); 440 } 441 /** 442 * send reply without payload. 443 * This function is thread-safe. 444 * @param out 445 * @param command 446 * @param errorCode 447 * @throws IOException 448 */ sendSimpleReplyHeader(int command, int errorCode)449 private void sendSimpleReplyHeader(int command, int errorCode) throws IOException { 450 Log.d(TAG, "sending reply cmd " + command + " err " + errorCode); 451 sendReplyWithData(command, errorCode, 0, null); 452 } 453 sendReplyWithData(int cmd, int errorCode, int len, byte[] data)454 private void sendReplyWithData(int cmd, int errorCode, int len, byte[] data) throws IOException { 455 try { 456 mClientLock.lock(); 457 mReplyBuffer.clear(); 458 mReplyBuffer.putInt((cmd & 0xffff) | 0x43210000); 459 mReplyBuffer.putInt(errorCode); 460 mReplyBuffer.putInt(len); 461 462 if (mOutput != null) { 463 mOutput.write(mReplyBuffer.array(), 0, PROTOCOL_SIMPLE_REPLY_SIZE); 464 if (data != null) { 465 mOutput.write(data, 0, len); 466 } 467 } 468 } catch (IOException e) { 469 throw e; 470 } finally { 471 mClientLock.unlock(); 472 } 473 } 474 private class LoopThread extends Thread { 475 private Looper mLooper; LoopThread(Runnable runnable)476 LoopThread(Runnable runnable) { 477 super(runnable); 478 } run()479 public void run() { 480 Looper.prepare(); 481 mLooper = Looper.myLooper(); 482 Log.d(TAG, "run runnable"); 483 super.run(); 484 //Log.d(TAG, "loop"); 485 Looper.loop(); 486 } 487 // should be called outside this thread quitLoop()488 public void quitLoop() { 489 mLooper.quit(); 490 try { 491 if (Thread.currentThread() != this) { 492 join(); 493 } 494 } catch (InterruptedException e) { 495 // ignore 496 } 497 Log.d(TAG, "quit thread"); 498 } 499 } 500 501 private class ProtocolServer implements Runnable { 502 503 @Override run()504 public void run() { 505 ServerSocket server = null; 506 507 try { // for catching exception from ServerSocket 508 Log.d(TAG, "get new server socket"); 509 server = new ServerSocket(PORT_NUMBER); 510 server.setReuseAddress(true); 511 server.setSoTimeout(SOCKET_ACCESS_TIMEOUT); 512 while (!mExitRequested) { 513 //TODO check already active recording/playback 514 try { // for catching exception from Socket, will restart upon exception 515 try { 516 mClientLock.lock(); 517 //Log.d(TAG, "will accept"); 518 mClient = server.accept(); 519 mClient.setReuseAddress(true); 520 mInput = mClient.getInputStream(); 521 mOutput = mClient.getOutputStream(); 522 } catch (SocketTimeoutException e) { 523 // This will happen frequently if client does not connect. 524 // just re-start 525 continue; 526 } finally { 527 mClientLock.unlock(); 528 } 529 Log.i(TAG, "new client connected"); 530 while (!mExitRequested) { 531 read(mInput, mHeaderBuffer, PROTOCOL_HEADER_SIZE); 532 int command = mHeaderBuffer.getInt(); 533 int len = mHeaderBuffer.getInt(); 534 Log.i(TAG, "received command " + command); 535 switch(command) { 536 case CMD_DOWNLOAD: 537 handleDownload(len); 538 break; 539 case CMD_START_PLAYBACK: 540 handleStartPlayback(len); 541 break; 542 case CMD_STOP_PLAYBACK: 543 handleStopPlayback(len); 544 break; 545 case CMD_START_RECORDING: 546 handleStartRecording(len); 547 break; 548 case CMD_STOP_RECORDING: 549 handleStopRecording(len); 550 break; 551 case CMD_GET_DEVICE_INFO: 552 handleGetDeviceInfo(len); 553 } 554 } 555 } catch (IOException e) { 556 Log.e(TAG, "restart from exception", e); 557 } catch (ProtocolError e) { 558 Log.e(TAG, "restart from exception", e); 559 } finally { 560 reset(); 561 } 562 } 563 } catch (ExitRequest e) { 564 Log.e(TAG, "exit requested, will exit", e); 565 } catch (IOException e) { 566 // error in server socket, just exit the thread and let things fail. 567 Log.e(TAG, "error while init, will exit", e); 568 } finally { 569 if (server != null) { 570 try { 571 server.close(); 572 } catch (IOException e) { 573 // ignore 574 } 575 } 576 reset(); 577 } 578 } 579 } 580 } 581