前言
在开始如何在安卓系统渲染原始PCM音频数据之前,先学习和了解与平台有关的基础知识。在安卓设备,一般的播放MP3格式音频,不管网络的还是本地的,通过MediaPlayer即可很方便实现,这里不再阐述。但是在直播应用场景中,都是渲染PCM裸音频数据,如何实现呢?有两种方式,方式一、通过Java层API AudioTrack渲染;方式二、通过native层的OpenSL ES渲染,OpenSL ES是嵌跨平台的嵌入式音频处理库,目前安卓系统接入的Open SL ES版本是1.0.1,这两种方式都是比较底层的API,需要比较多的配置方式,同时也比较灵活。
准备知识
.采样率
采集音频数据的频率。通俗点讲就是一秒钟有多少次采样,采样频率一般从8000hz-48000hz;
.采样位宽
每个采样的音频数据的表示精度,一般有8位,16位,32位;
.声道
单声道,双声道,多声道等等。单声道一次采样对应一个音频数据,双声道则一次采样对应两个音频数据,多声道则一次采样对应多个音频数据;拿44100hz采样率来说,如果采样位宽为16、1秒钟的音频数据大小为:44100x2x2个字节
.音频帧
这个概率是对于音频编码和音频渲染来说的。它表示一块音频数据(通常包含多个音频数据);对于音频编码来说,音频帧指的是每次编码包含的音频数据个数,不同的编码方式,每个音频帧包含的音频数据个数也不一样,比如aac编码一帧包含1024个音频数据,MP3编码一帧则包含1152个音频数据;对于音频渲染来说,音频帧指的是app一次发送给音频渲染系统的音频数据个数,具体个数由APP自己决定,一般取10-20ms数据为宜。
.音频存储序
当采用位宽为16位、32位时,就涉及到音频存储序的问题。可以采用大端序,也可以采用小端序。对于安卓渲染来说,只能播放小端序,ios则不受限制
.重采样之降采样和升采样
改变一段音频的采样率成为重采样。改变之后采样率大于原来的采样率成为上采样,小于则成为下采样。重采样是通过特点的插值算法增加或减少采样音频数据的过程,所以重采样往往会造成音频质量的损失。
本文介绍如何通过AudioTrack来播放PCM裸音频数据
目标
用AudioTrack渲染PCM音频,这里播放的PCM文件是通过ffmpeg解码到文件后的裸数据进行模拟
使用步骤
1、初始化AudioTrack
方式一:
这是官方推荐的初始化方法
/** 初始化AudioTrack,官方推荐此方法
* ch_layout:声道类型
* format:采样格式 表示一个采样点使用多少位表示
* sampleRate:一般有20khz,44.1khz 48khz等
* */
private void initAudioTrackByBuilder(int ch_layout,int sampleRate,int format) {
mMinBufferSize = AudioTrack.getMinBufferSize(sampleRate, ch_layout, format);
aTrack = new AudioTrack.Builder()
// AudioAttributes用来设置音频类型,相当于上面的streamType,如下是播放音频的策略
.setAudioAttributes(new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)// 这两项一般都是对应设置
.build())
.setAudioFormat(new AudioFormat.Builder()
.setEncoding(format) // 采样格式
.setSampleRate(sampleRate) // 采样率
.setChannelMask(ch_layout) // 就是声道类型
.build())
.setBufferSizeInBytes(mMinBufferSize)
.build();
}
mMinBufferSize = AudioTrack.getMinBufferSize(sampleRate, ch_layout, format);
计算最小的缓冲区的大小,官方建议使用此方式计算,而非自己手动计算
方式二:
/** 初始化AudioTrack
* ch_layout:声道类型
* format:采样格式 表示一个采样点使用多少位表示
* sampleRate:一般有20khz,44.1khz 48khz等
* */
private void initAudioTrack(int ch_layout,int sampleRate,int format) {
// 最好使用此函数计算缓冲区大小,而非自己手动计算
mMinBufferSize = AudioTrack.getMinBufferSize(sampleRate, ch_layout, format);
/**
* int streamType:表示了不同的音频播放策略,按下手机的音量键,可以看到有多个音量管理,比如可以单独禁止警告音但是可以开启
* 乐播放声音,这就是不同的音频播放管理策略;以常量形式定义在AudioManager中,如下:
* STREAM_MUSIC:播放音频用这个就好
* STREAM_VOICE_CALL:电话声音
* STREAM_ALARM:警告音
* ......
* int sampleRateInHz:音频采样率
* int channelConfig:声道类型;CHANNEL_IN_XXX适用于录制音频,CHANNEL_OUT_XXX用于播放音频
* int audioFormat:采样格式
* int bufferSizeInBytes:音频会话的缓冲区大小。音频播放时,app将音频原始数据不停的输送给这个缓冲区,然后AudioTrack不停从这个缓冲区拿数据送给音频播放系统
* 从而实现声音的播放
* int mode:缓冲区数据的流动方式;如下:
* MODE_STREAM:流式流动,只缓存部分
* MODE_STATIC:一次性缓冲全部数据,适用于音频比较小的播放
* 备注:对于录制音频,为了性能考虑,最好用CHANNEL_IN_MoNo单声道,而转变立体声的过程在声音的特效处理阶段来完成
* */
aTrack = new AudioTrack(
AudioManager.STREAM_MUSIC,// 指定流的类型
sampleRate,// 设置音频数据的採样率 32k,假设是44.1k就是44100
ch_layout,// 设置输出声道为双声道立体声,而CHANNEL_OUT_MONO类型是单声道
format,// 设置音频数据块是8位还是16位。这里设置为16位。
mMinBufferSize,//缓冲区大小
AudioTrack.MODE_STREAM // 设置模式类型,在这里设置为流类型,第二种MODE_STATIC貌似没有什么效果
);
}
2、启动播放
AudioTrack初始化之后就可以调用播放接口开始播放音频数据了
// 将AudioTrack切换到播放状态
public void play() {
isStop = false;
if (aTrack != null && aTrack.getState() != AudioTrack.STATE_UNINITIALIZED) {
aTrack.play();
startAudioThread();
}
}
3、输送音频数据
要想实现流畅播放,输送音频数据必须单独一个线程中,如下:
private class AudioThread implements Runnable {
@Override
public void run() {
DDlog.logd("AudioThread start mMinBufferSize==> "+mMinBufferSize);
// 一次写入的数据可以是1024,不一定非得mMinBufferSize个字节
// samples = new short[mMinBufferSize];
while (!isStop) {
try {
byte[] buffer = new byte[1024];
int sampleSize = mInputStream.read(buffer);
// DDlog.logd(ByteUtil.byte2hex(buffer));
// 向缓冲区写入数据,此函数为阻塞行数,一般写入200ms数据需要接近200ms时间
if (mFormat == AudioFormat.ENCODING_PCM_FLOAT) {
float[] samples = ByteUtil.bytesToFloats(buffer, sampleSize, isBigendian);
aTrack.write(samples,0,samples.length,AudioTrack.WRITE_BLOCKING);
} else if (mFormat == AudioFormat.ENCODING_PCM_16BIT) {
short[] samples = ByteUtil.bytesToShorts(buffer, sampleSize, isBigendian);
aTrack.write(samples, 0, samples.length);
} else {
aTrack.write(buffer, 0, sampleSize);
}
} catch (IOException io) {
}
}
}
}
这里要注意的一点就是,如果是直接从PCM文件中音频存储序是大端序方式,则还需要转换为小端序;并且还要将bytes[]数组转换成对应的shorts[]数组(如果播放16位音频),floats[]数组(如果播放32位float音频),转换代码如下:
// 将byte[] 数组转换成short[]数组
public static short[] bytesToShorts(byte[] bytes,int len,boolean isBe) {
if(bytes==null){
return null;
}
short[] shorts = new short[len/2];
// 大端序
if (isBe) {
ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).asShortBuffer().get(shorts);
} else {
ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().get(shorts);
}
return shorts;
}
// 将byte[] 数组转换成float[]数组
public static float[] bytesToFloats(byte[] bytes,int len,boolean isBe) {
if(bytes==null){
return null;
}
float[] floats = new float[len/4];
// 大端序
if (isBe) {
ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).asFloatBuffer().get(floats);
} else {
ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer().get(floats);
}
return floats;
}
4、停止播放
// 停止播放
if (aTrack != null && aTrack.getState() != AudioTrack.STATE_UNINITIALIZED) {
aTrack.stop();
}
// 释放AudioTrack
aTrack.release();
aTrack = null;
总结:
.遇到问题:
1、对于输入数据为大端序无法正常播放
.解决方案:
AudioTrack只能播放小端序的音频数据,所以对于大端序的数据得先转换成小端序在播放
2、输入数据格式为float类型无法正常播放
.解决方案:
由于inputStream读取的是字节,而当播放float类型数据时,write()函数写入的必须是float数组,所以写入之前要将byte[]数据转换成float[]数据
3、安卓对于8位数据的渲染不正常
项目地址
Demo