终于迎来了蓝牙a2dp的第二篇:利用AudioTrack播放PCM音频数据。如想查看更多内容,请点击《Android蓝牙开发系列文章-策划篇》。
先回顾一下上一篇文章《Android蓝牙开发系列文章-蓝牙音箱连接》讲到的蓝牙音箱的完成配对、连接的流程:扫描设备--监听DEVICE_FOUND广播-->直到找到目标设备-->对目标设备发起配对-->监听到设备配对成功-->发起设备连接-->监听连接状态的广播,连接成功。
本篇基于上一节的小结果,实现播放PCM数据,蓝牙音箱出声音。
音乐播放器播放的声音数据,经过解码、混音等处理后,送给a2dp_hw的数据就是PCM数据,也是这点联系,所以写了这篇文章。
目录
1.常见的音乐播放方式有哪些?
2.利用AudioTrack实现播放音频
2.1.配置基本参数
2.2获取最小缓冲区大小
2.3 基于基本参数、缓冲区创建AudioTrack对象
2.4 读取PCM文件,转成DataInputStream
2.5开启/停止播放
我了解到的常见的音乐播放方式有如下三种,如果你知道更多,请留言告诉我哈~
方式 | 特点 |
SoundPool | 适用于播放短促的声音,例如游戏音效、按键音等 |
AudioTrack | 仅适用于播放PCM音频数据 |
MediaPlayer | 能够播放多种文件格式的音频数据,例如MP3/AAC/WAV、OGG等。MediaPlayer在framework层创建AudioTrack,音频数据经过解码得到PCM数据,PCM数据再送到AudioTrack |
一个应用同一时刻可以创建多个AudioTrack,每个AudioTrack会注册到AudioFlinger中,所以应用的AudioTrack传输到AudioFlinger 中完成混音,然后输送到AudioHardware中进行播放。
Android同一时候最多能够创建32个音频流。
先上一下代码:
//设置音频流类型
private static final int mStreamType = AudioManager.STREAM_MUSIC;
//指定采样率
private static final int mSampleRateInHz=44100 ;
//指定捕获音频的声道数目。在AudioFormat类中指定用于此的常量
private static final int mChannelConfig= AudioFormat.CHANNEL_IN_STEREO; //双声道
//指定音频量化位数
private static final int mAudioFormat=AudioFormat.ENCODING_PCM_16BIT;
//因为我们的PCM文件较大,所欲选择载入方式为MODE_STREAM
private static int mMode = AudioTrack.MODE_STREAM;
音频流类型:
音频流常见的有如下几种,这里引出一个问题:为什么要区分出来这么多种类型?可能这样做的好处有很多哈,我get到的一个好处是:这样可以实现多不同场景下的音频数据进行区分控制,例如,在播放音乐时调小了音量,这个操作会对AudioManager.STREAM_MUSIC产生影响,但不会对其他类型音频起作用,假设这个时候有电话进来,电话铃声的音量跟播放音乐的音量是不一样的。
类型 | 解释 |
AudioManager.STREAM_MUSIC | 用于音乐播放的音频流 |
AudioManager.STREAM_SYSTEM | 用于系统声音的音频流 |
AudioManager.STREAM_RING | 用于电话铃声的音频流 |
AudioManager.STREAM_VOICE_CALL | 用于电话通话的音频流 |
AudioManager.STREAM_ALARM | 用于电话警报的音频流 |
AudioManager.STREAM_NOTIFICATION | 用于通知的音频流 |
AudioManager.STREAM_BLUETOOTH_SCO | 用于连接蓝牙耳机时的音频流 |
AudioManager.STREAM_TTS | 用于文本到音频转换的音频流 |
采样频率:
是指一秒钟内对模拟信号的采样次数,结合本文来说,就是获取到这个PCM音频实的采样频率,采样频率越高,越能够还原原始数据,但是会使得采样得到的文件体积更大。
奈奎斯特采样理论:当对被采样的模拟信号进行还原时,其最高频率只有采样频率的一半,换句话说,如果我们要完整还原原始的模拟信号,则采样频率就必须是它的两倍以上。
由于人耳所能辨识的声音范围是20-20KHZ,所以人们一般都选用44.1KHZ(CD)、48KHZ或者96KHZ来做为采样频率。
采样位数
它是用来衡量声音波动变化的一个参数,也可以说是声卡的分辨率。它的数值越大,分辨率也就越高,所发出声音的能力越强。下图是采样位数为4位的示意图,数值范围为0~15,采样数值分别为7,9,11,12,13,14,15,14.......。到采样位数越大时,能够表达的数值范围也就越大,量化得到的值也就越接近原始数据。
音频载入方式:
有两种音频载入方式,他们之间的区别如下:
音频载入方式 | 优缺点 |
AudioTrack.MODE_STREAM | 将PCM数据分一次次放入AudioTrack,适用于大的PCM数据,会产生一定的延迟问题 |
AudioTrack.MODE_STATIC | 将PCM数据一次性放入AudioTrack,适用于小的PCM数据和要求时延小的场景 |
看到这里可以在回头看一下上面的代码,看是否都以理解了上面的概念。此时,你也会问:你怎么知道这些参数取什么值合适的?
其实,我是利用Cool Edit Pro这个工具试出来的,打开Cool Edit Pro,选择文件找到我们的PCM文件,然后选择一组采样格式,然后点击“>”开始播放,听一下声音是否正常,如果正常,则说明你选择的参数是合理的(但不一定是最佳)。
这个缓冲区大小一定要通过AudioTrack::getMinBufferSize()来获取,一定不要自己附一个值。这个值与采样率、通道数、采样位数有关,具体计算公式在这里就不细究了,毕竟本专题是讲解蓝牙的~
mMinBufferSize = AudioTrack.getMinBufferSize(mSampleRateInHz,mChannelConfig, mAudioFormat);//计算最小缓冲区
mAudioTrack = new AudioTrack(mStreamType, mSampleRateInHz,mChannelConfig,
mAudioFormat,mMinBufferSize,mMode);
Log.d(TAG, "intData, mAudioTrack.getState() = " + mAudioTrack.getState());
可以看到AudioTrack有三种状态,在创建后就处于STATE_INITIALIZED状态,也就是说明:上面的log输出为:
03-13 23:37:45.647 12027-12027/com.atlas.btdemo D/MusicPlayer: intData, mAudioTrack.getState() = 1
/**
* State of an AudioTrack that was not successfully initialized upon creation.
*/
public static final int STATE_UNINITIALIZED = 0;
/**
* State of an AudioTrack that is ready to be used.
*/
public static final int STATE_INITIALIZED = 1;
/**
* State of a successfully initialized AudioTrack that uses static data,
* but that hasn't received that data yet.
*/
public static final int STATE_NO_STATIC_DATA = 2;
我们在res目录下创建一个名为raw的文件夹,然后将pcm文件放进去,在代码中访问该文件的方法如下:
private DataInputStream mDis;//播放文件的数据流
mDis = new DataInputStream(context.getResources().openRawResource(R.raw.pcm_test));
因为我用PCM文件有10M多,在安装应用的过程中十分耗时,这一点很不好,不知道有什么办法可以解决?
我们采用一个最笨的办法:自己记录一个标记,在开始播放时,将该标记设置成true,在停止播放时,将该标记设置成false。
private boolean isStart = false;
public boolean isMusicPlaying() {
if(isStart == true) {
return true;
} else {
return false;
}
}
/**
* 销毁线程方法
*/
private void destroyThread() {
try {
isStart = false;
if (null != mRecordThread && Thread.State.RUNNABLE == mRecordThread.getState()) {
try {
Thread.sleep(500);
mRecordThread.interrupt();
} catch (Exception e) {
mRecordThread = null;
}
}
mRecordThread = null;
} catch (Exception e) {
e.printStackTrace();
} finally {
mRecordThread = null;
}
}
/**
* 启动播放线程
*/
private void startThread() {
destroyThread();
isStart = true;
if (mRecordThread == null) {
mRecordThread = new Thread(recordRunnable);
mRecordThread.start();
}
}
/**
* 播放线程
*/
Runnable recordRunnable = new Runnable() {
@Override
public void run() {
try {
//设置线程的优先级
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_URGENT_AUDIO);
byte[] tempBuffer = new byte[mMinBufferSize];
int readCount = 0;
while (mDis.available() > 0) {
readCount= mDis.read(tempBuffer);
if (readCount == AudioTrack.ERROR_INVALID_OPERATION || readCount == AudioTrack.ERROR_BAD_VALUE) {
continue;
}
if (readCount != 0 && readCount != -1) {//一边播放一边写入语音数据
//判断AudioTrack未初始化,停止播放的时候释放了,状态就为STATE_UNINITIALIZED
if(mAudioTrack.getState() == mAudioTrack.STATE_UNINITIALIZED){
initData();
}
mAudioTrack.play();
mAudioTrack.write(tempBuffer, 0, readCount);
}
}
stopPlay();//播放完就停止播放
} catch (Exception e) {
e.printStackTrace();
}
}
};
其实还有一个更好的办法是通过如下接口来判断,
/**
* Returns the playback state of the AudioTrack instance.
* @see #PLAYSTATE_STOPPED
* @see #PLAYSTATE_PAUSED
* @see #PLAYSTATE_PLAYING
*/
public int getPlayState() {
synchronized (mPlayStateLock) {
switch (mPlayState) {
case PLAYSTATE_STOPPING:
return PLAYSTATE_PLAYING;
case PLAYSTATE_PAUSED_STOPPING:
return PLAYSTATE_PAUSED;
default:
return mPlayState;
}
}
}
那什么时刻触发音乐播放或者停止呢?
我们在蓝牙音箱连接成功后,就去开始播放音乐。在点击UI上的PLAY_PCM实现音乐播放状态的反转:播放-》暂停,或者暂停-》播放。
if (action.equals(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED)) {
BluetoothDevice btdevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
int preConnectionState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, 0);
int newConnectionState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, 0);
Log.d(TAG, "btdevice = " + btdevice.getName() + ", preConnectionState = "
+ preConnectionState + ", newConnectionState" + newConnectionState);
if(newConnectionState == BluetoothProfile.STATE_CONNECTED && preConnectionState == BluetoothProfile.STATE_CONNECTING) {
Log.d(TAG, "target device connect success");
if(mMusicPlayer != null && mMusicPlayer.isMusicPlaying()) {
Log.d(TAG, "music is playing");
// mHandler.sendEmptyMessage(MSG_START_PLAYPCM);
mHandler.sendEmptyMessageDelayed(MSG_STOP_PLAYPCM, DELAYT_TIMES);
} else {
Log.d(TAG, "music play has stopped");
// mHandler.sendEmptyMessage(MSG_STOP_PLAYPCM);
mHandler.sendEmptyMessageDelayed(MSG_START_PLAYPCM,DELAYT_TIMES);
}
}
}
@Override
public void onClick(View view) {
Log.d(TAG, "view id = " + view.getId());
switch (view.getId()) {
case R.id.bt_scan:
Log.d(TAG, "start bt scan");
mHandler.sendEmptyMessageDelayed(MSG_SCAN, DELAYT_TIMES);
break;
case R.id.bt_connect:
initProfileProxy();
Log.d(TAG, "start bt connect");
mHandler.sendEmptyMessageDelayed(MSG_CONNECT, DELAYT_TIMES);
break;
case R.id.bt_playpcm:
if(mMusicPlayer != null && mMusicPlayer.isMusicPlaying()) {
Log.d(TAG, "music is playing");
// mHandler.sendEmptyMessage(MSG_START_PLAYPCM);
mHandler.sendEmptyMessageDelayed(MSG_STOP_PLAYPCM, DELAYT_TIMES);
} else {
Log.d(TAG, "music play has stopped");
// mHandler.sendEmptyMessage(MSG_STOP_PLAYPCM);
mHandler.sendEmptyMessageDelayed(MSG_START_PLAYPCM, DELAYT_TIMES);
}
break;
default:
break;
}
本想给大家录一段音频,可是娃都睡了,就不录了~
如果想持续关注本博客内容,请扫描关注个人微信公众号,或者微信搜索:万物互联技术。