blob: 32cc3ed0ff4a8baf9358de9ceda807ddb55c284c [file] [log] [blame]
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.cts.audiotest;
import android.app.Activity;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.MediaRecorder.AudioSource;
import android.media.AudioTrack;
import android.os.Looper;
import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.Thread;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.concurrent.locks.ReentrantLock;
public class AudioProtocol implements AudioTrack.OnPlaybackPositionUpdateListener {
private static final String TAG = "AudioProtocol";
private static final int PORT_NUMBER = 15001;
private Thread mThread = new Thread(new ProtocolServer());
private boolean mExitRequested = false;
private static final int PROTOCOL_HEADER_SIZE = 8; // id + payload length
private static final int MAX_NON_DATA_PAYLOAD_SIZE = 20;
private static final int PROTOCOL_SIMPLE_REPLY_SIZE = 12;
private static final int PROTOCOL_OK = 0;
private static final int PROTOCOL_ERROR_WRONG_PARAM = 1;
private static final int PROTOCOL_ERROR_GENERIC = 2;
private static final int CMD_DOWNLOAD = 0x12340001;
private static final int CMD_START_PLAYBACK = 0x12340002;
private static final int CMD_STOP_PLAYBACK = 0x12340003;
private static final int CMD_START_RECORDING = 0x12340004;
private static final int CMD_STOP_RECORDING = 0x12340005;
private ByteBuffer mHeaderBuffer = ByteBuffer.allocate(PROTOCOL_HEADER_SIZE);
private ByteBuffer mDataBuffer = ByteBuffer.allocate(MAX_NON_DATA_PAYLOAD_SIZE);
private ByteBuffer mReplyBuffer = ByteBuffer.allocate(PROTOCOL_SIMPLE_REPLY_SIZE);
// all socket access (accept / read) set this timeout to check exit periodically.
private static final int SOCKET_ACCESS_TIMEOUT = 2000;
private Socket mClient = null;
private InputStream mInput = null;
private OutputStream mOutput = null;
// lock to use to write to socket, I/O streams, and also change socket (create, destroy)
private ReentrantLock mClientLock = new ReentrantLock();
private AudioRecord mRecord = null;
private LoopThread mRecordThread = null;
private AudioTrack mPlayback = null;
private LoopThread mPlaybackThread = null;
// store recording length
private int mRecordingLength = 0;
// map for playback data
private HashMap<Integer, ByteBuffer> mDataMap = new HashMap<Integer, ByteBuffer>();
public boolean start() {
Log.d(TAG, "start");
mExitRequested = false;
mThread.start();
//Log.d(TAG, "started");
return true;
}
public void stop() throws InterruptedException {
Log.d(TAG, "stop");
mExitRequested = true;
try {
mClientLock.lock();
if (mClient != null) {
// wake up from socket read
mClient.shutdownInput();
}
}catch (IOException e) {
// ignore
} finally {
mClientLock.unlock();
}
mThread.interrupt(); // this does not bail out from socket in android
mThread.join();
reset();
Log.d(TAG, "stopped");
}
@Override
public void onMarkerReached(AudioTrack track) {
Log.d(TAG, "playback completed");
try {
sendSimpleReplyHeader(CMD_START_PLAYBACK, PROTOCOL_OK);
} catch (IOException e) {
// maybe socket already closed. don't do anything
Log.e(TAG, "ignore exception", e);
} finally {
track.stop();
track.flush();
track.release();
mPlaybackThread.quitLoop(false);
mPlaybackThread = null;
}
}
@Override
public void onPeriodicNotification(AudioTrack arg0) {
Log.d(TAG, "track periodic notification");
// TODO Auto-generated method stub
}
/**
* Read given amount of data to the buffer
* @param in
* @param buffer
* @param len length to read
* @return true if header read successfully, false if exit requested
* @throws IOException
* @throws ExitRequest
*/
private void read(InputStream in, ByteBuffer buffer, int len) throws IOException, ExitRequest {
buffer.clear();
int totalRead = 0;
while (totalRead < len) {
int readNow = in.read(buffer.array(), totalRead, len - totalRead);
if (readNow < 0) { // end-of-stream, error
Log.e(TAG, "read returned " + readNow);
throw new IOException();
}
totalRead += readNow;
if(mExitRequested) {
throw new ExitRequest();
}
}
}
private class ProtocolError extends Exception {
public ProtocolError(String message) {
super(message);
}
}
private class ExitRequest extends Exception {
public ExitRequest() {
super();
}
}
private void assertProtocol(boolean cond, String message) throws ProtocolError {
if (!cond) {
throw new ProtocolError(message);
}
}
private void reset() {
// lock only when it is not already locked by this thread
if (mClientLock.getHoldCount() == 0) {
mClientLock.lock();
}
if (mClient != null) {
try {
mClient.close();
} catch (IOException e) {
// ignore
}
mClient = null;
}
mInput = null;
mOutput = null;
while (mClientLock.getHoldCount() > 0) {
mClientLock.unlock();
}
if (mRecord != null) {
if (mRecord.getState() != AudioRecord.STATE_UNINITIALIZED) {
mRecord.stop();
}
mRecord.release();
mRecord = null;
}
if (mRecordThread != null) {
mRecordThread.quitLoop(true);
mRecordThread = null;
}
if (mPlayback != null) {
if (mPlayback.getState() != AudioTrack.STATE_UNINITIALIZED) {
mPlayback.stop();
mPlayback.flush();
}
mPlayback.release();
mPlayback = null;
}
if (mPlaybackThread != null) {
mPlaybackThread.quitLoop(true);
mPlaybackThread = null;
}
mDataMap.clear();
}
private void handleDownload(int len) throws IOException, ExitRequest {
read(mInput, mDataBuffer, 4); // only for id
Integer id = new Integer(mDataBuffer.getInt(0));
int dataLength = len - 4;
ByteBuffer data = ByteBuffer.allocate(dataLength);
read(mInput, data, dataLength);
mDataMap.put(id, data);
Log.d(TAG, "downloaded data id " + id + " len " + dataLength);
sendSimpleReplyHeader(CMD_DOWNLOAD, PROTOCOL_OK);
}
private void handleStartPlayback(int len) throws ProtocolError, IOException, ExitRequest {
// this error is too critical, so do not even send reply
assertProtocol(len == 20, "wrong payload len");
read(mInput, mDataBuffer, len);
final Integer id = new Integer(mDataBuffer.getInt(0));
final int samplingRate = mDataBuffer.getInt(1 * 4);
final boolean stereo = ((mDataBuffer.getInt(2 * 4) & 0x80000000) != 0);
final int mode = mDataBuffer.getInt(2 * 4) & 0x7fffffff;
final int volume = mDataBuffer.getInt(3 * 4);
final int repeat = mDataBuffer.getInt(4 * 4);
try {
final ByteBuffer data = mDataMap.get(id);
if (data == null) {
throw new ProtocolError("wrong id");
}
if (samplingRate != 44100) {
throw new ProtocolError("wrong rate");
}
//FIXME cannot start playback again
//TODO repeat
//FIXME in MODE_STATIC, setNotificationMarkerPosition does not work with full length
mPlaybackThread = new LoopThread(new Runnable() {
@Override
public void run() {
if (mPlayback != null) {
mPlayback.release();
mPlayback = null;
}
int type = (mode == 0) ? AudioManager.STREAM_VOICE_CALL :
AudioManager.STREAM_MUSIC;
mPlayback = new AudioTrack(type, samplingRate,
stereo ? AudioFormat.CHANNEL_OUT_STEREO : AudioFormat.CHANNEL_OUT_MONO,
AudioFormat.ENCODING_PCM_16BIT, data.capacity(),
AudioTrack.MODE_STREAM);
float minVolume = mPlayback.getMinVolume();
float maxVolume = mPlayback.getMaxVolume();
float newVolume = (maxVolume - minVolume) * volume / 100 + minVolume;
mPlayback.setStereoVolume(newVolume, newVolume);
Log.d(TAG, "setting volume " + newVolume + " max " + maxVolume +
" min " + minVolume + " received " + volume);
mPlayback.write(data.array(), 0, data.capacity());
mPlayback.setPlaybackPositionUpdateListener(AudioProtocol.this);
int endMarker = data.capacity()/(stereo ? 4 : 2);
int res = mPlayback.setNotificationMarkerPosition(endMarker);
Log.d(TAG, "start playback id " + id + " len " + data.capacity() +
" set.. res " + res + " stereo? " + stereo + " mode " + mode +
" end " + endMarker);
mPlayback.play();
}
});
mPlaybackThread.start();
// send reply when play is completed
} catch (ProtocolError e) {
sendSimpleReplyHeader(CMD_START_PLAYBACK, PROTOCOL_ERROR_WRONG_PARAM);
Log.e(TAG, "wrong param", e);
}
}
private void handleStopPlayback(int len) throws ProtocolError, IOException {
Log.d(TAG, "stopPlayback");
assertProtocol(len == 0, "wrong payload len");
if (mPlayback != null) {
Log.d(TAG, "release AudioTrack");
mPlayback.stop();
mPlayback.flush();
mPlayback.release();
mPlayback = null;
}
if (mPlaybackThread != null) {
mPlaybackThread.quitLoop(true);
mPlaybackThread = null;
}
sendSimpleReplyHeader(CMD_STOP_PLAYBACK, PROTOCOL_OK);
}
private void handleStartRecording(int len) throws ProtocolError, IOException, ExitRequest {
assertProtocol(len == 16, "wrong payload len");
read(mInput, mDataBuffer, len);
final int samplingRate = mDataBuffer.getInt(0);
final boolean stereo = ((mDataBuffer.getInt(1 * 4) & 0x80000000) != 0);
final int mode = mDataBuffer.getInt(1 * 4) & 0x7fffffff;
final int volume = mDataBuffer.getInt(2 * 4);
final int samples = mDataBuffer.getInt(3 * 4);
try {
if (samplingRate != 44100) {
throw new ProtocolError("wrong rate");
}
if (stereo) {
throw new ProtocolError("mono only");
}
//TODO volume ?
mRecordingLength = samples * 2;
mRecordThread = new LoopThread(new Runnable() {
@Override
public void run() {
int minBufferSize = AudioRecord.getMinBufferSize(samplingRate,
AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);
int type = (mode == 0) ? AudioSource.VOICE_RECOGNITION : AudioSource.DEFAULT;
mRecord = new AudioRecord(type, samplingRate,
AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT,
(minBufferSize > mRecordingLength) ? minBufferSize : mRecordingLength);
mRecord.startRecording();
Log.d(TAG, "recording started " + " samples " + samples + " mode " + mode +
" recording state " + mRecord.getRecordingState() + " len " +
mRecordingLength);
try {
boolean recordingOk = true;
byte[] data = new byte[mRecordingLength];
int totalRead = 0;
while (totalRead < mRecordingLength) {
int lenRead = mRecord.read(data, 0, (mRecordingLength - totalRead));
if (lenRead < 0) {
Log.e(TAG, "reading recording failed with error code " + lenRead);
recordingOk = false;
break;
} else if (lenRead == 0) {
Log.w(TAG, "zero read");
}
totalRead += lenRead;
}
Log.d(TAG, "reading recording completed");
mClientLock.lock();
mReplyBuffer.clear();
mReplyBuffer.putInt((CMD_START_RECORDING & 0xffff) | 0x43210000);
mReplyBuffer.putInt(recordingOk ? PROTOCOL_OK : PROTOCOL_ERROR_GENERIC);
mReplyBuffer.putInt(recordingOk ? mRecordingLength : 0);
if (mOutput != null) {
mOutput.write(mReplyBuffer.array(), 0, PROTOCOL_SIMPLE_REPLY_SIZE);
if (recordingOk) {
mOutput.write(data, 0, mRecordingLength);
}
}
} catch (IOException e) {
// maybe socket already closed. don't do anything
Log.e(TAG, "ignore exception", e);
} finally {
mRecord.stop();
mRecord.release();
mRecord = null;
mClientLock.unlock();
}
}
});
mRecordThread.start();
} catch (ProtocolError e) {
sendSimpleReplyHeader(CMD_START_RECORDING, PROTOCOL_ERROR_WRONG_PARAM);
Log.e(TAG, "wrong param", e);
}
}
private void handleStopRecording(int len) throws ProtocolError, IOException {
Log.d(TAG, "stop recording");
assertProtocol(len == 0, "wrong payload len");
if (mRecord != null) {
mRecord.stop();
mRecord.release();
mRecord = null;
}
if (mRecordThread != null) {
mRecordThread.quitLoop(true);
mRecordThread = null;
}
sendSimpleReplyHeader(CMD_STOP_RECORDING, PROTOCOL_OK);
}
/**
* send reply without payload.
* This function is thread-safe.
* @param out
* @param command
* @param errorCode
* @throws IOException
*/
private void sendSimpleReplyHeader(int command, int errorCode) throws IOException {
Log.d(TAG, "sending reply cmd " + command + " err " + errorCode);
try {
mClientLock.lock();
mReplyBuffer.clear();
mReplyBuffer.putInt((command & 0xffff) | 0x43210000);
mReplyBuffer.putInt(errorCode);
mReplyBuffer.putInt(0); // payload length
if (mOutput != null) {
mOutput.write(mReplyBuffer.array(), 0, PROTOCOL_SIMPLE_REPLY_SIZE);
}
} finally {
mClientLock.unlock();
}
}
private class LoopThread extends Thread {
private Looper mLooper;
LoopThread(Runnable runnable) {
super(runnable);
}
public void run() {
Looper.prepare();
mLooper = Looper.myLooper();
Log.d(TAG, "run runnable");
super.run();
//Log.d(TAG, "loop");
Looper.loop();
}
// should be called outside this thread
public void quitLoop(boolean wait) {
mLooper.quit();
try {
if (wait) {
join();
}
} catch (InterruptedException e) {
// ignore
}
Log.d(TAG, "quit thread");
}
}
private class ProtocolServer implements Runnable {
@Override
public void run() {
ServerSocket server = null;
try { // for catching exception from ServerSocket
Log.d(TAG, "get new server socket");
server = new ServerSocket(PORT_NUMBER);
server.setReuseAddress(true);
server.setSoTimeout(SOCKET_ACCESS_TIMEOUT);
while (!mExitRequested) {
//TODO check already active recording/playback
try { // for catching exception from Socket, will restart upon exception
try {
mClientLock.lock();
//Log.d(TAG, "will accept");
mClient = server.accept();
mClient.setReuseAddress(true);
mInput = mClient.getInputStream();
mOutput = mClient.getOutputStream();
} catch (SocketTimeoutException e) {
// This will happen frequently if client does not connect.
// just re-start
continue;
} finally {
mClientLock.unlock();
}
Log.i(TAG, "new client connected");
while (!mExitRequested) {
read(mInput, mHeaderBuffer, PROTOCOL_HEADER_SIZE);
int command = mHeaderBuffer.getInt();
int len = mHeaderBuffer.getInt();
Log.i(TAG, "received command " + command);
switch(command) {
case CMD_DOWNLOAD:
handleDownload(len);
break;
case CMD_START_PLAYBACK:
handleStartPlayback(len);
break;
case CMD_STOP_PLAYBACK:
handleStopPlayback(len);
break;
case CMD_START_RECORDING:
handleStartRecording(len);
break;
case CMD_STOP_RECORDING:
handleStopRecording(len);
break;
}
}
} catch (IOException e) {
Log.e(TAG, "restart from exception", e);
} catch (ProtocolError e) {
Log.e(TAG, "restart from exception", e);
} finally {
reset();
}
}
} catch (ExitRequest e) {
Log.e(TAG, "exit requested, will exit", e);
} catch (IOException e) {
// error in server socket, just exit the thread and let things fail.
Log.e(TAG, "error while init, will exit", e);
} finally {
if (server != null) {
try {
server.close();
} catch (IOException e) {
// ignore
}
}
reset();
}
}
}
}