Home | History | Annotate | Download | only in audiotest
      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 
     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 
     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
    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
    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      */
    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 {
    160         public ProtocolError(String message) {
    161             super(message);
    162         }
    163     }
    164 
    165     private class ExitRequest extends Exception {
    166         public ExitRequest() {
    167             super();
    168         }
    169     }
    170 
    171     private void assertProtocol(boolean cond, String message) throws ProtocolError {
    172         if (!cond) {
    173             throw new ProtocolError(message);
    174         }
    175     }
    176 
    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 
    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 
    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 
    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 
    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 
    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 
    417     private void appendAttrib(StringBuilder builder, String name, String value) {
    418         builder.append(" " + name + "=\"" + value + "\"");
    419     }
    420 
    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      */
    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 
    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;
    476         LoopThread(Runnable runnable) {
    477             super(runnable);
    478         }
    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
    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
    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