blob: 12291ee557f0000860b8511902dc18938bfd203b [file] [log] [blame]
/*
* Copyright (C) 2014 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 org.drrickorang.loopback;
import android.content.Context;
import android.media.AudioDeviceInfo;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.AudioTrack;
import android.media.MediaRecorder;
import android.os.Build;
import android.util.Log;
import android.os.Handler;
import android.os.Message;
/**
* A thread/audio track based audio synth.
*/
public class LoopbackAudioThread extends Thread {
private static final String TAG = "LoopbackAudioThread";
private static final int THREAD_SLEEP_DURATION_MS = 1;
// for latency test
static final int LOOPBACK_AUDIO_THREAD_MESSAGE_LATENCY_REC_STARTED = 991;
static final int LOOPBACK_AUDIO_THREAD_MESSAGE_LATENCY_REC_ERROR = 992;
static final int LOOPBACK_AUDIO_THREAD_MESSAGE_LATENCY_REC_COMPLETE = 993;
static final int LOOPBACK_AUDIO_THREAD_MESSAGE_LATENCY_REC_STOP = 994;
// for buffer test
static final int LOOPBACK_AUDIO_THREAD_MESSAGE_BUFFER_REC_STARTED = 996;
static final int LOOPBACK_AUDIO_THREAD_MESSAGE_BUFFER_REC_ERROR = 997;
static final int LOOPBACK_AUDIO_THREAD_MESSAGE_BUFFER_REC_COMPLETE = 998;
static final int LOOPBACK_AUDIO_THREAD_MESSAGE_BUFFER_REC_STOP = 999;
public boolean mIsRunning = false;
public AudioTrack mAudioTrack;
public int mSessionId;
private Thread mRecorderThread;
private RecorderRunnable mRecorderRunnable;
private final int mSamplingRate;
private final int mChannelIndex;
private final int mChannelConfigIn = AudioFormat.CHANNEL_IN_MONO;
private final int mAudioFormat = AudioFormat.ENCODING_PCM_16BIT;
private int mMinPlayerBufferSizeInBytes = 0;
private int mMinRecorderBuffSizeInBytes = 0;
private int mMinPlayerBufferSizeSamples = 0;
private final int mMicSource;
private final int mChannelConfigOut = AudioFormat.CHANNEL_OUT_MONO;
private boolean mIsPlaying = false;
private boolean mIsRequestStop = false;
private Handler mMessageHandler;
// This is the pipe that connects the player and the recorder in latency test.
private PipeShort mLatencyTestPipe = new PipeShort(Constant.MAX_SHORTS);
// for buffer test
private BufferPeriod mRecorderBufferPeriod; // used to collect recorder's buffer period
private BufferPeriod mPlayerBufferPeriod; // used to collect player's buffer period
private int mTestType; // latency test or buffer test
private int mBufferTestDurationInSeconds; // Duration of actual buffer test
private Context mContext;
private int mBufferTestWavePlotDurationInSeconds;
private final CaptureHolder mCaptureHolder;
private boolean mIsAdjustingSoundLevel = true; // only used in buffer test
public static TestSettings computeDefaultSettings() {
int samplingRate = AudioTrack.getNativeOutputSampleRate(AudioManager.STREAM_MUSIC);
int minPlayerBufferSizeInBytes = AudioTrack.getMinBufferSize(samplingRate,
AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT);
int minRecorderBufferSizeInBytes = AudioRecord.getMinBufferSize(samplingRate,
AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);
return new TestSettings(samplingRate, minPlayerBufferSizeInBytes,
minRecorderBufferSizeInBytes);
}
public LoopbackAudioThread(int samplingRate, int playerBufferInBytes, int recorderBufferInBytes,
int micSource, BufferPeriod recorderBufferPeriod,
BufferPeriod playerBufferPeriod, int testType,
int bufferTestDurationInSeconds,
int bufferTestWavePlotDurationInSeconds, Context context,
int channelIndex, CaptureHolder captureHolder) {
mSamplingRate = samplingRate;
mMinPlayerBufferSizeInBytes = playerBufferInBytes;
mMinRecorderBuffSizeInBytes = recorderBufferInBytes;
mMicSource = micSource;
mRecorderBufferPeriod = recorderBufferPeriod;
mPlayerBufferPeriod = playerBufferPeriod;
mTestType = testType;
mBufferTestDurationInSeconds = bufferTestDurationInSeconds;
mBufferTestWavePlotDurationInSeconds = bufferTestWavePlotDurationInSeconds;
mContext = context;
mChannelIndex = channelIndex;
mCaptureHolder = captureHolder;
setName("Loopback_LoopbackAudio");
}
public void run() {
setPriority(Thread.MAX_PRIORITY);
if (mMinPlayerBufferSizeInBytes <= 0) {
mMinPlayerBufferSizeInBytes = AudioTrack.getMinBufferSize(mSamplingRate,
mChannelConfigOut, mAudioFormat);
log("Player: computed min buff size = " + mMinPlayerBufferSizeInBytes + " bytes");
} else {
log("Player: using min buff size = " + mMinPlayerBufferSizeInBytes + " bytes");
}
mMinPlayerBufferSizeSamples = mMinPlayerBufferSizeInBytes / Constant.BYTES_PER_FRAME;
short[] audioShortArrayOut = new short[mMinPlayerBufferSizeSamples];
// we may want to adjust this to different multiplication of mMinPlayerBufferSizeSamples
int audioTrackWriteDataSize = mMinPlayerBufferSizeSamples;
// used for buffer test only
final double frequency1 = Constant.PRIME_FREQUENCY_1;
final double frequency2 = Constant.PRIME_FREQUENCY_2; // not actually used
short[] bufferTestTone = new short[audioTrackWriteDataSize]; // used by AudioTrack.write()
ToneGeneration toneGeneration = new SineWaveTone(mSamplingRate, frequency1);
mRecorderRunnable = new RecorderRunnable(mLatencyTestPipe, mSamplingRate, mChannelConfigIn,
mAudioFormat, mMinRecorderBuffSizeInBytes, MediaRecorder.AudioSource.MIC, this,
mRecorderBufferPeriod, mTestType, frequency1, frequency2,
mBufferTestWavePlotDurationInSeconds, mContext, mChannelIndex, mCaptureHolder);
mRecorderRunnable.setBufferTestDurationInSeconds(mBufferTestDurationInSeconds);
mRecorderThread = new Thread(mRecorderRunnable);
mRecorderThread.setName("Loopback_RecorderRunnable");
// both player and recorder run at max priority
mRecorderThread.setPriority(Thread.MAX_PRIORITY);
mRecorderThread.start();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mAudioTrack = new AudioTrack.Builder()
.setAudioFormat((mChannelIndex < 0 ?
new AudioFormat.Builder().setChannelMask(AudioFormat.CHANNEL_OUT_MONO) :
new AudioFormat.Builder().setChannelIndexMask(1 << mChannelIndex))
.setSampleRate(mSamplingRate)
.setEncoding(mAudioFormat)
.build())
.setBufferSizeInBytes(mMinPlayerBufferSizeInBytes)
.setTransferMode(AudioTrack.MODE_STREAM)
.build();
} else {
mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
mSamplingRate,
mChannelConfigOut,
mAudioFormat,
mMinPlayerBufferSizeInBytes,
AudioTrack.MODE_STREAM /* FIXME runtime test for API level 9,
mSessionId */);
}
if (mRecorderRunnable != null && mAudioTrack.getState() == AudioTrack.STATE_INITIALIZED) {
mIsPlaying = false;
mIsRunning = true;
while (mIsRunning && mRecorderThread.isAlive()) {
if (mIsPlaying) {
switch (mTestType) {
case Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_LATENCY:
// read from the pipe and plays it out
int samplesAvailable = mLatencyTestPipe.availableToRead();
if (samplesAvailable > 0) {
int samplesOfInterest = Math.min(samplesAvailable,
mMinPlayerBufferSizeSamples);
int samplesRead = mLatencyTestPipe.read(audioShortArrayOut, 0,
samplesOfInterest);
mAudioTrack.write(audioShortArrayOut, 0, samplesRead);
mPlayerBufferPeriod.collectBufferPeriod();
}
break;
case Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_BUFFER_PERIOD:
// don't collect buffer period when we are still adjusting the sound level
if (mIsAdjustingSoundLevel) {
toneGeneration.generateTone(bufferTestTone, bufferTestTone.length);
mAudioTrack.write(bufferTestTone, 0, audioTrackWriteDataSize);
} else {
mPlayerBufferPeriod.collectBufferPeriod();
toneGeneration.generateTone(bufferTestTone, bufferTestTone.length);
mAudioTrack.write(bufferTestTone, 0, audioTrackWriteDataSize);
}
break;
}
} else {
// wait for a bit to allow AudioTrack to start playing
if (mIsRunning) {
try {
sleep(THREAD_SLEEP_DURATION_MS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
endTest();
} else {
log("Loopback Audio Thread couldn't run!");
mAudioTrack.release();
mAudioTrack = null;
if (mMessageHandler != null) {
Message msg = Message.obtain();
switch (mTestType) {
case Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_LATENCY:
msg.what = LOOPBACK_AUDIO_THREAD_MESSAGE_LATENCY_REC_ERROR;
break;
case Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_BUFFER_PERIOD:
msg.what = LOOPBACK_AUDIO_THREAD_MESSAGE_BUFFER_REC_ERROR;
break;
}
mMessageHandler.sendMessage(msg);
}
}
}
public void setMessageHandler(Handler messageHandler) {
mMessageHandler = messageHandler;
}
public void setIsAdjustingSoundLevel(boolean isAdjustingSoundLevel) {
mIsAdjustingSoundLevel = isAdjustingSoundLevel;
}
public void runTest() {
if (mIsRunning) {
// start test
if (mAudioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) {
log("...run test, but still playing...");
endTest();
} else {
// start playing
mIsPlaying = true;
mAudioTrack.play();
boolean status = mRecorderRunnable.startRecording();
log("Started capture test");
if (mMessageHandler != null) {
Message msg = Message.obtain();
msg.what = LOOPBACK_AUDIO_THREAD_MESSAGE_LATENCY_REC_STARTED;
if (!status) {
msg.what = LOOPBACK_AUDIO_THREAD_MESSAGE_LATENCY_REC_ERROR;
}
mMessageHandler.sendMessage(msg);
}
}
}
}
public void runBufferTest() {
if (mIsRunning) {
// start test
if (mAudioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING) {
log("...run test, but still playing...");
endTest();
} else {
// start playing
mIsPlaying = true;
mAudioTrack.play();
boolean status = mRecorderRunnable.startBufferRecording();
log(" Started capture test");
if (mMessageHandler != null) {
Message msg = Message.obtain();
msg.what = LOOPBACK_AUDIO_THREAD_MESSAGE_BUFFER_REC_STARTED;
if (!status) {
msg.what = LOOPBACK_AUDIO_THREAD_MESSAGE_BUFFER_REC_ERROR;
}
mMessageHandler.sendMessage(msg);
}
}
}
}
/** Clean some things up before sending out a message to LoopbackActivity. */
public void endTest() {
switch (mTestType) {
case Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_LATENCY:
log("--Ending latency test--");
break;
case Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_BUFFER_PERIOD:
log("--Ending buffer test--");
break;
}
mIsPlaying = false;
mAudioTrack.pause();
mLatencyTestPipe.flush();
mAudioTrack.flush();
if (mMessageHandler != null) {
Message msg = Message.obtain();
if (mIsRequestStop) {
switch (mTestType) {
case Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_LATENCY:
msg.what = LOOPBACK_AUDIO_THREAD_MESSAGE_LATENCY_REC_STOP;
break;
case Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_BUFFER_PERIOD:
msg.what = LOOPBACK_AUDIO_THREAD_MESSAGE_BUFFER_REC_STOP;
break;
}
} else {
switch (mTestType) {
case Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_LATENCY:
msg.what = LOOPBACK_AUDIO_THREAD_MESSAGE_LATENCY_REC_COMPLETE;
break;
case Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_BUFFER_PERIOD:
msg.what = LOOPBACK_AUDIO_THREAD_MESSAGE_BUFFER_REC_COMPLETE;
break;
}
}
mMessageHandler.sendMessage(msg);
}
}
/**
* This is called only when the user requests to stop the test through
* pressing a button in the LoopbackActivity.
*/
public void requestStopTest() throws InterruptedException {
mIsRequestStop = true;
mRecorderRunnable.requestStop();
}
/** Release mAudioTrack and mRecorderThread. */
public void finish() throws InterruptedException {
mIsRunning = false;
final AudioTrack at = mAudioTrack;
if (at != null) {
at.release();
mAudioTrack = null;
}
Thread zeThread = mRecorderThread;
mRecorderThread = null;
if (zeThread != null) {
zeThread.interrupt();
zeThread.join(Constant.JOIN_WAIT_TIME_MS);
}
}
private static void log(String msg) {
Log.v(TAG, msg);
}
public double[] getWaveData() {
return mRecorderRunnable.getWaveData();
}
public int[] getAllGlitches() {
return mRecorderRunnable.getAllGlitches();
}
public boolean getGlitchingIntervalTooLong() {
return mRecorderRunnable.getGlitchingIntervalTooLong();
}
public int getFFTSamplingSize() {
return mRecorderRunnable.getFFTSamplingSize();
}
public int getFFTOverlapSamples() {
return mRecorderRunnable.getFFTOverlapSamples();
}
int getDurationInSeconds() {
return mBufferTestDurationInSeconds;
}
}