Android SDK 提供了3套音频播放的API,分别是:MediaPlayer,SoundPool,AudioTrack,本文重点说下AudioTrack。
前面讲到了MediaPlayer、SoundPool但是AudioTrack和其也有很多不同的特性,下面我们会进行对比。
参见AudioTrack的官方文档其有如下主要特性:
从MediaPlayer的方法调用查看,底层都会通过IPC机制调到MediaPlayerService。不仅是MediaPlayer,android.media包下的媒体播放接口像AudioTrack、SoundPool都是会调到MediaPlayerService来进行编解码操作,安卓的媒体播放是个典型的C/S架构。
【官方文档】媒体应用根据以下架构与 Android 原生多媒体框架进行交互。
媒体架构MediaPlayer可以播放多种格式的声音文件,如MP3,AAC,WAV,OGG,MIDI等,、AudioTrack只能播放PCM数据流。
MediaPlayer在播放音频时,在framework层还是会创建AudioTrack,把解码后的PCM数流传递给AudioTrack,最后由AudioFlinger进行混音,传递音频给硬件播放出来。
AudioTrack播放原理是没有Mediaplayer的解码部分,所以只能播放PCM数据流。
Mediaplayer的解码核心部分是OpenMAX接口来进行扩展。
如果使用AudioTrack播放mp3文件的话,要自己加入一个音频解码器,如libmad。否则只能播放PCM数据即大多数WAV格式的音频文件。
如上图媒体架构中:
Binder IPC:
Binder IPC 代理用于促进跨越进程边界的通信。这些代理位于 frameworks/av/media/libmedia 目录中,并以字母“I”开头。
原生多媒体框架
在原生层,Android 提供了一个利用 Stagefright 引擎进行音频和视频录制及播放的多媒体框架。Stagefright 随附支持的软件编解码器的默认列表,并且您可以使用 OpenMax 集成层标准实现自己的硬件编解码器。如需了解实现的更多详情,请参阅位于 frameworks/av/media 中的 MediaPlayer 和 Stagefright 组件。
OpenMAX 集成层 (IL)
OpenMAX IL 为 Stagefright 提供了一种标准化的方式识别和使用基于硬件的自定义多媒体编解码器(称为组件)。您必须以名为 libstagefrighthw.so 的共享库的形式提供 OpenMAX 插件。此插件将 Stagefright 与您的自定义编解码器组件相连接,并且该组件必须根据 OpenMAX IL 组件标准实现。
确定音频流的最小缓冲区大小。要做到这一点,需要知道采样率,数据是单声道还是立体声,以及是否使用8位或者16位PCM编码。然后以采样率和采样大小作为参数调用AudioTrack.getMinBufferSize(),该方法会以字节形式返回AudioTrack实例的最小缓冲区大小。
MediaPlayer提供了5个setDataSource方法,如其中一个,虽然可以设置文件流起始地址与文件流长度;但是对于实时地播放加密过的音频文件却是束手无策。虽然对于一些加密过的音频文件,可以采用Audiotrack与Libmad结合的方式解决。因为AudioTrack在流模式下,应用程序使用write()方法之一向AudioTrack写入连续的数据流。加密后的数据可以获取到流,适用于该特性。
Android 音频架构定义了音频功能的实现方式,并指出实现中所涉及的相关源代码。如果感兴趣可以参看官网文档查阅相关源码:
Android 音频架构AudioTrack有两种初始化方式:
1、public AudioTrack(int streamType, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes, int mode)
2、AudioTrack.Builder
Android M(6.0)之后舍弃了public AudioTrack(int streamType, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes, int mode)的方式初始化,所以要兼容6.0以上需要使用AudioTrack.Builder初始化
streamType:
参数streamType:媒体类型STREAM_VOICE_CALL, STREAM_SYSTEM, STREAM_RING, STREAM_MUSIC, STREAM_ALARM, STREAM_NOTIFICATION
sampleRateInHz:
参数sampleRateInHz:采样率,有8000、20100等,一般来说越高音质越好,但文件体积就越大
channelConfig:
参数channelConfig:声道,单声道CHANNEL_OUT_MONO 和双声道 CHANNEL_OUT_STEREO
audioFormat:
参数audioFormat:采样点大小,只有ENCODING_PCM_16BIT 和 ENCODING_PCM_8BIT两种选择,意思是一个采集点16bit或8bit
bufferSizeInBytes:
参数bufferSizeInBytes:AudioTrack一次所能接收最小的声音资源大小,通过getMinBufferSize函数获取,用于读取音频数据的内部缓冲区的总大小(以byte为单位)。
mode:
参数mode:有MODE_STATIC和MODE_STREAM两种分类。
setAudioAttributes 设置AudioAttributes 实例,不能为空
setUsage 设置 AudioTrack 的使用场景;
setContentType 设置输入的音频文件内容的类型;
setAudioFormat AudioFormat是 用于描述播放的数据格式,AudioFormat 中包含了编码格式,声道和采样率等,不能为空
setEncoding 设置 采样格式
setSampleRate 设置采样率
setChannelMask设置声道
setTransferMode int模式二选一 MODE_STATIC 与 MODE_STREAM
静态模式(MODE_STATIC) & 流模式(MODE_STREAM)
在流模式下,应用程序使用write()方法之一向AudioTrack写入连续的数据流。
当数据从Java层传输到本机层并排队等待回放时,它们会阻塞并返回。
流媒体模式在播放音频数据块时最有用
当处理适合内存且需要以最小延迟播放的短声音时,应该选择静态模式。
因此,静态模式更适合那些经常玩的UI和游戏声音,并且开销尽可能小。
setBufferSizeInBytes setBufferSizeInBytes:int: 用于读取音频数据的内部缓冲区的总大小(以byte为单位)。
如果 mode 是 MODE_STATIC ,其为音频最大长度;如果是 MODE_STREAM ,其值要大于等于接收流的最小缓冲区大小, 建议使用 getMinBufferSize(int, int, int) 方法来估算 AudioTrack的实例在流模式下的最小缓冲区大小.
private AudioTrack mAudioTrack;
private int SMPL = 44100;
private int mode = AudioTrack.MODE_STREAM;
//获取最小缓冲区大小
int minBufferSize = AudioTrack.getMinBufferSize(SMPL,//采样率
AudioFormat.CHANNEL_OUT_STEREO, //双声道
AudioFormat.ENCODING_PCM_16BIT //采样格式
);
public void initAudioTrack(){
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
//AudioTrack.Builder
mAudioTrack = new AudioTrack.Builder()
//1、setAudioAttributes:AudioAttributes 实例,不能为空
.setAudioAttributes(new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)//setUsage 设置 AudioTrack 的使用场景;
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)//setContentType 设置输入的音频文件内容的类型;
.build())
//2、setAudioFormat:AudioFormat是 用于描述播放的数据格式,AudioFormat 中包含了编码
//格式,声道和采样率等,不能为空
.setAudioFormat(new AudioFormat.Builder()
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)//采样格式
.setSampleRate(SMPL)//设置采样率
.setChannelMask(AudioFormat.CHANNEL_OUT_STEREO)//设置声道
.build())
//3、setTransferMode:int模式二选一 MODE_STATIC 与 MODE_STREAM
.setTransferMode(mode)
//4、setBufferSizeInBytes:int: 用于读取音频数据的内部缓冲区的总大小(以byte为单位)。
//如果 mode 是 MODE_STATIC ,其为音频最大长度;
//如果是 MODE_STREAM ,其值要大于等于接收流的最小缓冲区大小,建议使用 getMinBufferSize(int, int, int) 方法来估算 AudioTrack的实例在流模式下的最小缓冲区大小
.setBufferSizeInBytes(minBufferSize)
.build();
} else {
//public AudioTrack(int streamType, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes, int mode)
mAudioTrack = new AudioTrack(
//1、参数streamType:媒体类型有STREAM_VOICE_CALL, STREAM_SYSTEM, STREAM_RING, STREAM_MUSIC, STREAM_ALARM, STREAM_NOTIFICATION
AudioManager.STREAM_MUSIC
//2、参数sampleRateInHz:采样率,有8000、20100等,一般来说越高音质越好,但文件体积就越大
, SMPL
//3、参数channelConfig:声道,单声道CHANNEL_OUT_MONO 和双声道 CHANNEL_OUT_STEREO
, AudioFormat.CHANNEL_IN_STEREO//双声道
//4、参数audioFormat:采样点大小,只有ENCODING_PCM_16BIT 和 ENCODING_PCM_8BIT两种选择,意思是一个采集点16bit或8bit
, AudioFormat.ENCODING_PCM_16BIT//采样格式
//5、参数bufferSizeInBytes:AudioTrack一次所能接收最小的声音资源大小,通过getMinBufferSize函数获取,
, minBufferSize//缓冲区大小
//6、参数mode:有MODE_STATIC和MODE_STREAM两种分类。
, mode
);
}
}
在没有看文档之前,我不太清楚调用的顺序。
开始播放声音的时机,直觉上以为是play,没想到是调用write。
这还挺另类。
方法 | 目的 |
---|---|
play | 将playState设置为播放状态(PLAYSTATE_PLAYING) |
stop | 将playState置于停止状态(PLAYSTATE_STOPPED)。 |
write | 将数据写入播放缓冲区。 |
flush | 清除播放缓冲区中的数据。 |
release | 销毁AudioTrack对象。 |
在实现逻辑中,initAudioTrack→ready之后,重复play。
通过预处理来设置数据,
然后用reloadStaticData()将静态缓冲区内的播放头位置设置为零,即将其回退到静态缓冲区的开始位置。
重复播放相同模式的音频
这个好像比较合适。
虽然每次都stop(),但是没有stop()也能连续播放。
/**
* 准备数据:MODE_STATIC
* @param data
*/
private void readyModeStatic(byte[] data){
//在缓存器中预先设置数据
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
mAudioTrack.write(data, 0, data.length, AudioTrack.WRITE_BLOCKING);
}else{
mAudioTrack.write(data, 0, data.length);
}
}
/**
* 播放:MODE_STATIC
* @param data
*/
private void playModeStatic(byte[] data){
if(mAudioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING){
//播放中停止播放
mAudioTrack.stop();
//清除播放器缓存器
mAudioTrack.flush();
}
//重新读取准备数据
mAudioTrack.reloadStaticData();
//向播放缓冲器写入数据
readyModeStatic(data);
//播放
mAudioTrack.play();
}
如果播放完成后没有停止(),将不会输出声音。
另外,如果缓冲区的内容已用完,则会发生错误,并且将跳过一次播放。
我通过用预处理填充缓冲区来解决它
/**
* 准备数据:MODE_STREAM
* @param data
*/
private void readyModeStream(byte[] data){
if(mAudioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING){
mAudioTrack.stop();
mAudioTrack.flush();
}
if(mAudioTrack != null){
//填充缓冲区
int loopCount = minBufferSize / data.length;
for (int i = 0 ; i < loopCount ; i++){
//在缓存器中预先设置数据
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
mAudioTrack.write(data, 0, data.length, AudioTrack.WRITE_BLOCKING);
}else{
mAudioTrack.write(data, 0, data.length);
}
}
}
}
/**
* 播放:MODE_STREAM
* @param data
*/
private void playModeStream(byte[] data){
readyModeStream(data);
//监听播放中停止
mAudioTrack.setPlaybackPositionUpdateListener(new AudioTrack.OnPlaybackPositionUpdateListener() {
@Override
public void onMarkerReached(AudioTrack track) {
if(track.getPlayState() == AudioTrack.PLAYSTATE_PLAYING){
track.stop();
}
}
@Override
public void onPeriodicNotification(AudioTrack track) {
}
});
mAudioTrack.setNotificationMarkerPosition(data.length);
//进入播放状态
if(mAudioTrack.getPlayState() != AudioTrack.PLAYSTATE_PLAYING){
mAudioTrack.play();
}
}
//停止
private void stop(){
if(mAudioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING){
//播放中停止播放
mAudioTrack.stop();
//清除播放缓冲器
mAudioTrack.flush();
}
}
//在结束时释放对象
private void release(){
try {
this.stop();
mAudioTrack.release();
mAudioTrack = null;
}catch (Exception e){
e.printStackTrace();
}
}
您可以使用write方法指定WRITE_BLOCKING和WRITE_NON_BLOCKING。
WRITE_BLOCKING等待下一个播放,直到上一个播放完成为止,并且 WRITE_NON_BLOCKING就像在播放下一个播放一样,即使上一个播放尚未完成。
当设置为WRITE_NON_BLOCKING时,重复时, 最后一个音频和第一个音频可能会重叠并播放。
在此要求下,必须以规则的时间间隔重复播放,因此通过使用 WRITE_BLOCKING每次都停止播放,开始时间不会发生变化。
推荐阅读
heiyulong,公众号:Android进化之路Android SDK 提供的3套音频播放的API之玩转SoundPool
推荐阅读
heiyulong,公众号:Android进化之路Android SDK 提供的3套音频播放的API之玩转MediaPlayer
长按二维码识别
「Android进化之路」