Android 音频录音/播放系列:
《Android AudioRecord 录音封装及测试》
《Android AudioTrack 播放封装及测试》
对 Android AudioTrack 封装,内部管理播放状态,并在暂停播放时做 FadeOut,在恢复播放时做 FadeIn ,避免快速暂停/恢复的播放杂音问题。
并设计成回调的拉模式,通过回调向外层要播放的数据。
AudioPlayer 封装如下:
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import com.alan.audioio.audio.common.IDataAvailableListener;
import com.alan.audioio.audio.common.IOStatus;
import com.alan.audioio.audio.common.Type;
import com.alan.audioio.audio.exception.AudioException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
* Author: AlanWang4523.
* Date: 2019-06-14 23:04.
* Mail: alanwang4523@gmail.com
*/
public class AudioPlayer {
private final static String TAG = AudioPlayer.class.getSimpleName();
private IDataAvailableListener mDataAvailableListener;
private volatile @IOStatus int mNewStatus;
private volatile @IOStatus int mCurStatus;
private boolean mIsStatusChanged = false;
private Thread mWorkThread;
private AudioTrack mAudioTrack;
private int mChannelCount;
private ByteBuffer mDataBuffer;
private final ReentrantLock mLock = new ReentrantLock();
private final Condition mStatusCondition = mLock.newCondition();
private final Condition mPlayStateCondition = mLock.newCondition();
/**
* 构造函数
*/
public AudioPlayer() {
mNewStatus = IOStatus.UNINITIATED;
mCurStatus = IOStatus.UNINITIATED;
}
/**
* 设置 IDataAvailableListener
* @param dataAvailableListener 用于获取要播放的数据
*/
public void setDataAvailableListener(IDataAvailableListener dataAvailableListener) {
this.mDataAvailableListener = dataAvailableListener;
}
/**
* 初始化
* @param sampleRateInHz 采样率
* @param channelCount 通道数
* @param bufferSize bufferSize
* @throws AudioException AudioException
*/
public void init(int sampleRateInHz, int channelCount, int bufferSize) throws AudioException {
try {
if (bufferSize <= 0) {
throw new AudioException("The buffer size must be greater than 0!", null);
}
int channelConfig = channelCount == Type.ChannelCount.Stereo ?
AudioFormat.CHANNEL_OUT_STEREO : AudioFormat.CHANNEL_OUT_MONO;
int audioFormat = AudioFormat.ENCODING_PCM_16BIT; // 默认采样 short 型格式
int minBufferSize = AudioTrack.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);
mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
sampleRateInHz, channelConfig, audioFormat, minBufferSize, AudioTrack.MODE_STREAM);
mChannelCount = channelCount;
mDataBuffer = ByteBuffer.allocateDirect(bufferSize).order(ByteOrder.LITTLE_ENDIAN);
mNewStatus = IOStatus.INITIATED;
mCurStatus = IOStatus.INITIATED;
} catch (Exception e) {
throw new AudioException("Init AudioPlayer Failed!", e);
}
}
/**
* 开始播放,只能在初始化成功后调用
*/
public void start() {
if (mNewStatus == IOStatus.INITIATED) {
mWorkThread = new Thread(null, new WorkRunnable(),
TAG + "-" + System.currentTimeMillis());
mNewStatus = IOStatus.START;
mIsStatusChanged = true;
mWorkThread.start();
mLock.lock();
try {
while (mIsStatusChanged) {
try {
mStatusCondition.await(1000, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
// do nothing
}
}
} finally {
mLock.unlock();
}
} else {
throw new IllegalStateException();
}
}
/**
* 暂停播放
*/
public void pause() {
if (mNewStatus == IOStatus.PAUSE || mNewStatus == IOStatus.STOP) {
return;
}
if (mNewStatus != IOStatus.START && mNewStatus != IOStatus.RESUME) {
return;
}
mLock.lock();
try {
mNewStatus = IOStatus.PAUSE;
mIsStatusChanged = true;
while (mIsStatusChanged) {
try {
mStatusCondition.await(1000, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
// do nothing
}
}
} finally {
mLock.unlock();
}
mAudioTrack.pause();
}
/**
* 恢复播放
*/
public void resume() {
if (mNewStatus != IOStatus.PAUSE) {
return;
}
mLock.lock();
try {
mAudioTrack.play();
mNewStatus = IOStatus.RESUME;
mIsStatusChanged = true;
mPlayStateCondition.signal();
while (mIsStatusChanged) {
try {
mStatusCondition.await(1000, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
// do nothing
}
}
} finally {
mLock.unlock();
}
}
/**
* 停止播放
*/
public void stop() {
if (mNewStatus == IOStatus.STOP || mNewStatus == IOStatus.UNINITIATED
|| mNewStatus == IOStatus.INITIATED) {
return;
}
mLock.lock();
try {
mNewStatus = IOStatus.STOP;
mIsStatusChanged = true;
// 需要调用 notify,避免在 pause 状态调用 stop 时,work thread 还在 wait
mPlayStateCondition.signal();
while (mIsStatusChanged) {
try {
mStatusCondition.await(1000, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
// do nothing
}
}
} finally {
mLock.unlock();
}
}
/**
* 释放资源
*/
public void release() {
mLock.lock();
try {
// 如果初始化后还没开始播放则释放资源,否则统一在 WorkRunnable 中释放
if (mNewStatus == IOStatus.INITIATED) {
mAudioTrack.release();
} else {
mNewStatus = IOStatus.STOP;
mIsStatusChanged = true;
// 需要调用 notify,避免在 pause 状态调用 release 时,work thread 还在 wait
mPlayStateCondition.signal();
}
} finally {
mLock.unlock();
}
// 等待工作线程结束
if (mWorkThread != null) {
try {
mWorkThread.join(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private class WorkRunnable implements Runnable {
@Override
public void run() {
mAudioTrack.play();
while (true) {
boolean isNeedFade = false;
mLock.lock();
if (mIsStatusChanged) {
mCurStatus = mNewStatus;
isNeedFade = true;
}
mLock.unlock();
mDataBuffer.clear();
if (mDataAvailableListener != null) {
// 外层将需要播放的数据放入 mDataBuffer
mDataAvailableListener.onDataAvailable(mDataBuffer);
}
ByteBuffer byteBuffer = mDataBuffer;
if (byteBuffer == null || byteBuffer.limit() <= 0) {
if (isNeedFade) {
mLock.lock();
mIsStatusChanged = false;
mStatusCondition.signal();
mLock.unlock();
}
continue;
}
byteBuffer.rewind();
// 如果状态发生改变,对播放数据做 Fade
if (isNeedFade) {
if (mCurStatus == IOStatus.PAUSE || mCurStatus == IOStatus.STOP) {
shortFadeOut(byteBuffer, mChannelCount);
} else {
shortFadeIn(byteBuffer, mChannelCount);
}
}
mAudioTrack.write(byteBuffer.array(), byteBuffer.arrayOffset(), byteBuffer.limit());
mLock.lock();
try {
if (isNeedFade) {
mIsStatusChanged = false;
mStatusCondition.signal();
}
while ((mCurStatus == IOStatus.PAUSE) && !mIsStatusChanged) {
try {
mPlayStateCondition.await();
} catch (InterruptedException e) {
// do nothing
}
}
if (mCurStatus == IOStatus.STOP || mCurStatus == IOStatus.UNINITIATED) {
break;
}
} finally {
mLock.unlock();
}
}
try {
mAudioTrack.stop();
} catch (IllegalStateException e) {
e.printStackTrace();
}
try {
mAudioTrack.release();
} catch (IllegalStateException e) {
e.printStackTrace();
}
}
}
/**
* 对音频数据做 fade out
* @param byteBuffer byteBuffer
* @param channelCount channelCount
*/
private void shortFadeOut(ByteBuffer byteBuffer, int channelCount) {
int shortCount = byteBuffer.limit() / 2;
if(1 == channelCount) {
for(int i = 0; i < shortCount; i++) {
short data = (short) (byteBuffer.getShort(i * 2) * 1.0f * (shortCount - i) / shortCount);
byteBuffer.putShort(i * 2, data);
}
} else {
for(int i = 0; i < shortCount; i += 2) {
short data = (short) (byteBuffer.getShort(i * 2) * 1.0f * (shortCount - i) / shortCount);
byteBuffer.putShort(i * 2, data);
data = (short)(byteBuffer.getShort((i + 1) * 2) * 1.0f * (shortCount - i) / shortCount);
byteBuffer.putShort((i + 1) * 2, data);
}
}
byteBuffer.rewind();
}
/**
* 对音频数据做 fade in
* @param byteBuffer byteBuffer
* @param channelCount channelCount
*/
private void shortFadeIn(ByteBuffer byteBuffer, int channelCount) {
int shortCount = byteBuffer.limit() / 2;
if(1 == channelCount) {
for(int i = 0; i < shortCount; i++) {
short data = (short)(byteBuffer.getShort(i * 2) * 1.0f * i / shortCount);
byteBuffer.putShort(i * 2, data);
}
} else {
for(int i = 0; i < shortCount; i += 2) {
short data = (short)(byteBuffer.getShort(i * 2) * 1.0f * i / shortCount);
byteBuffer.putShort(i * 2, data);
data = (short)(byteBuffer.getShort((i + 1) * 2) * 1.0f * i / shortCount);
byteBuffer.putShort((i + 1) * 2, data);
}
}
byteBuffer.rewind();
}
}
IOStatus 状态定义:
import androidx.annotation.IntDef;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Author: AlanWang4523.
* Date: 2020/11/10 21:19.
* Mail: alanwang4523@gmail.com
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({
IOStatus.UNINITIATED, IOStatus.INITIATED,
IOStatus.START, IOStatus.PAUSE,
IOStatus.RESUME, IOStatus.STOP})
public @interface IOStatus {
int UNINITIATED = -1;
int INITIATED = 0;
int START = 1;
int PAUSE = 2;
int RESUME = 3;
int STOP = 4;
}
播放数据回调接口定义:
import java.nio.ByteBuffer;
/**
* Author: AlanWang4523.
* Date: 2020/11/10 21:40.
* Mail: alanwang4523@gmail.com
*/
public interface IDataAvailableListener {
/**
* 有数据到来
* @param byteBuffer 具体数据
* 如果是来自采集端可以从 byteBuffer 读取数据,有效数据长度为 byteBuffer.limit()
* 如果是来自播放端可以往 byteBuffer 写入数据,写完后调用 byteBuffer.limit(count) 设置有效数据长度
*/
void onDataAvailable(ByteBuffer byteBuffer);
IOStatus 状态定义和数据回调接口定义同《Android AudioRecord 录音封装及测试》
测试代码:
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import com.alan.audioio.R;
import com.alan.audioio.audio.AudioPlayer;
import com.alan.audioio.audio.WavFile;
import com.alan.audioio.audio.common.IDataAvailableListener;
import com.alan.audioio.audio.exception.AudioException;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import androidx.appcompat.app.AppCompatActivity;
public class TestPlayWavActivity extends AppCompatActivity implements View.OnClickListener {
private String mPlayWavPath = "/sdcard/Alan/audio/record_wrapper.wav";
private AudioPlayer mAudioPlayer;
private WavFile mWavFile;
private TextView mBtnCommonTest;
private boolean mIsStartTest = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_common_test);
mBtnCommonTest = findViewById(R.id.btnCommonTest);
mBtnCommonTest.setOnClickListener(this);
mBtnCommonTest.setText("StarTest");
initAudioPlayer();
if (mAudioPlayer != null) {
mAudioPlayer.start();
}
mBtnCommonTest.setText("Pause");
mIsStartTest = true;
}
@Override
protected void onDestroy() {
if (mIsStartTest) {
mIsStartTest = false;
}
releaseAudioRes();
super.onDestroy();
}
@Override
public void onClick(View view) {
if (view.getId() == R.id.btnCommonTest) {
if (!mIsStartTest) {
mIsStartTest = true;
mBtnCommonTest.setText("Pause");
if (mAudioPlayer != null) {
mAudioPlayer.resume();
}
} else {
mIsStartTest = false;
mBtnCommonTest.setText("Resume");
if (mAudioPlayer != null) {
mAudioPlayer.pause();
}
}
}
}
private void initAudioPlayer() {
releaseAudioRes();
int sampleRate = 44100;
int channelCount = 1;
int bufferSize = 1024;
try {
File wavFile = new File(mPlayWavPath);
if (!wavFile.exists()) {
Toast.makeText(this, "The wav file is not exist.", Toast.LENGTH_SHORT).show();
return;
}
mWavFile = new WavFile(mPlayWavPath);
mAudioPlayer = new AudioPlayer();
mAudioPlayer.init(sampleRate, channelCount, bufferSize);
mAudioPlayer.setDataAvailableListener(new IDataAvailableListener() {
@Override
public void onDataAvailable(ByteBuffer byteBuffer) {
int readLen = -1;
try {
readLen = mWavFile.read(byteBuffer.array(), byteBuffer.arrayOffset(), byteBuffer.limit());
} catch (IOException e) {
e.printStackTrace();
}
if (readLen < 0) {
readLen = 0;
}
byteBuffer.limit(readLen);
}
});
} catch (AudioException | IOException e) {
e.printStackTrace();
Toast.makeText(this, "init failed.", Toast.LENGTH_SHORT).show();
finish();
}
}
public void releaseAudioRes() {
if (mAudioPlayer != null) {
mAudioPlayer.release();
}
if (mWavFile != null) {
try {
mWavFile.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
Android 音频录音/播放系列:
《Android AudioRecord 录音封装及测试》
《Android AudioTrack 播放封装及测试》
完整代码见:AudioIO