前言
在開始如何在安卓系統(tǒng)渲染原始PCM音頻數(shù)據(jù)之前,先學(xué)習(xí)和了解與平臺有關(guān)的基礎(chǔ)知識。在安卓設(shè)備,一般的播放MP3格式音頻,不管網(wǎng)絡(luò)的還是本地的,通過MediaPlayer即可很方便實現(xiàn),這里不再闡述。但是在直播應(yīng)用場景中,都是渲染PCM裸音頻數(shù)據(jù),如何實現(xiàn)呢?有兩種方式,方式一、通過Java層API AudioTrack渲染;方式二、通過native層的OpenSL ES渲染,OpenSL ES是嵌跨平臺的嵌入式音頻處理庫,目前安卓系統(tǒng)接入的Open SL ES版本是1.0.1,這兩種方式都是比較底層的API,需要比較多的配置方式,同時也比較靈活。
準備知識
.采樣率
采集音頻數(shù)據(jù)的頻率。通俗點講就是一秒鐘有多少次采樣,采樣頻率一般從8000hz-48000hz;
.采樣位寬
每個采樣的音頻數(shù)據(jù)的表示精度,一般有8位,16位,32位;
.聲道
單聲道,雙聲道,多聲道等等。單聲道一次采樣對應(yīng)一個音頻數(shù)據(jù),雙聲道則一次采樣對應(yīng)兩個音頻數(shù)據(jù),多聲道則一次采樣對應(yīng)多個音頻數(shù)據(jù);拿44100hz采樣率來說,如果采樣位寬為16、1秒鐘的音頻數(shù)據(jù)大小為:44100x2x2個字節(jié)
.音頻幀
這個概率是對于音頻編碼和音頻渲染來說的。它表示一塊音頻數(shù)據(jù)(通常包含多個音頻數(shù)據(jù));對于音頻編碼來說,音頻幀指的是每次編碼包含的音頻數(shù)據(jù)個數(shù),不同的編碼方式,每個音頻幀包含的音頻數(shù)據(jù)個數(shù)也不一樣,比如aac編碼一幀包含1024個音頻數(shù)據(jù),MP3編碼一幀則包含1152個音頻數(shù)據(jù);對于音頻渲染來說,音頻幀指的是app一次發(fā)送給音頻渲染系統(tǒng)的音頻數(shù)據(jù)個數(shù),具體個數(shù)由APP自己決定,一般取10-20ms數(shù)據(jù)為宜。
.音頻存儲序
當采用位寬為16位、32位時,就涉及到音頻存儲序的問題。可以采用大端序,也可以采用小端序。對于安卓渲染來說,只能播放小端序,ios則不受限制
.重采樣之降采樣和升采樣
改變一段音頻的采樣率成為重采樣。改變之后采樣率大于原來的采樣率成為上采樣,小于則成為下采樣。重采樣是通過特點的插值算法增加或減少采樣音頻數(shù)據(jù)的過程,所以重采樣往往會造成音頻質(zhì)量的損失。
本文介紹如何通過AudioTrack來播放PCM裸音頻數(shù)據(jù)
目標
用AudioTrack渲染PCM音頻,這里播放的PCM文件是通過ffmpeg解碼到文件后的裸數(shù)據(jù)進行模擬
使用步驟
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用來設(shè)置音頻類型,相當于上面的streamType,如下是播放音頻的策略
.setAudioAttributes(new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)// 這兩項一般都是對應(yīng)設(shè)置
.build())
.setAudioFormat(new AudioFormat.Builder()
.setEncoding(format) // 采樣格式
.setSampleRate(sampleRate) // 采樣率
.setChannelMask(ch_layout) // 就是聲道類型
.build())
.setBufferSizeInBytes(mMinBufferSize)
.build();
}
mMinBufferSize = AudioTrack.getMinBufferSize(sampleRate, ch_layout, format);
計算最小的緩沖區(qū)的大小,官方建議使用此方式計算,而非自己手動計算
方式二:
/** 初始化AudioTrack
* ch_layout:聲道類型
* format:采樣格式 表示一個采樣點使用多少位表示
* sampleRate:一般有20khz,44.1khz 48khz等
* */
private void initAudioTrack(int ch_layout,int sampleRate,int format) {
// 最好使用此函數(shù)計算緩沖區(qū)大小,而非自己手動計算
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:音頻會話的緩沖區(qū)大小。音頻播放時,app將音頻原始數(shù)據(jù)不停的輸送給這個緩沖區(qū),然后AudioTrack不停從這個緩沖區(qū)拿數(shù)據(jù)送給音頻播放系統(tǒng)
* 從而實現(xiàn)聲音的播放
* int mode:緩沖區(qū)數(shù)據(jù)的流動方式;如下:
* MODE_STREAM:流式流動,只緩存部分
* MODE_STATIC:一次性緩沖全部數(shù)據(jù),適用于音頻比較小的播放
* 備注:對于錄制音頻,為了性能考慮,最好用CHANNEL_IN_MoNo單聲道,而轉(zhuǎn)變立體聲的過程在聲音的特效處理階段來完成
* */
aTrack = new AudioTrack(
AudioManager.STREAM_MUSIC,// 指定流的類型
sampleRate,// 設(shè)置音頻數(shù)據(jù)的採樣率 32k,假設(shè)是44.1k就是44100
ch_layout,// 設(shè)置輸出聲道為雙聲道立體聲,而CHANNEL_OUT_MONO類型是單聲道
format,// 設(shè)置音頻數(shù)據(jù)塊是8位還是16位。這里設(shè)置為16位。
mMinBufferSize,//緩沖區(qū)大小
AudioTrack.MODE_STREAM // 設(shè)置模式類型,在這里設(shè)置為流類型,第二種MODE_STATIC貌似沒有什么效果
);
}
2、啟動播放
AudioTrack初始化之后就可以調(diào)用播放接口開始播放音頻數(shù)據(jù)了
// 將AudioTrack切換到播放狀態(tài)
public void play() {
isStop = false;
if (aTrack != null && aTrack.getState() != AudioTrack.STATE_UNINITIALIZED) {
aTrack.play();
startAudioThread();
}
}
3、輸送音頻數(shù)據(jù)
要想實現(xiàn)流暢播放,輸送音頻數(shù)據(jù)必須單獨一個線程中,如下:
private class AudioThread implements Runnable {
@Override
public void run() {
DDlog.logd("AudioThread start mMinBufferSize==> "+mMinBufferSize);
// 一次寫入的數(shù)據(jù)可以是1024,不一定非得mMinBufferSize個字節(jié)
// samples = new short[mMinBufferSize];
while (!isStop) {
try {
byte[] buffer = new byte[1024];
int sampleSize = mInputStream.read(buffer);
// DDlog.logd(ByteUtil.byte2hex(buffer));
// 向緩沖區(qū)寫入數(shù)據(jù),此函數(shù)為阻塞行數(shù),一般寫入200ms數(shù)據(jù)需要接近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文件中音頻存儲序是大端序方式,則還需要轉(zhuǎn)換為小端序;并且還要將bytes[]數(shù)組轉(zhuǎn)換成對應(yīng)的shorts[]數(shù)組(如果播放16位音頻),floats[]數(shù)組(如果播放32位float音頻),轉(zhuǎn)換代碼如下:
// 將byte[] 數(shù)組轉(zhuǎn)換成short[]數(shù)組
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[] 數(shù)組轉(zhuǎn)換成float[]數(shù)組
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;
總結(jié):
.遇到問題:
1、對于輸入數(shù)據(jù)為大端序無法正常播放
.解決方案:
AudioTrack只能播放小端序的音頻數(shù)據(jù),所以對于大端序的數(shù)據(jù)得先轉(zhuǎn)換成小端序在播放
2、輸入數(shù)據(jù)格式為float類型無法正常播放
.解決方案:
由于inputStream讀取的是字節(jié),而當播放float類型數(shù)據(jù)時,write()函數(shù)寫入的必須是float數(shù)組,所以寫入之前要將byte[]數(shù)據(jù)轉(zhuǎn)換成float[]數(shù)據(jù)
3、安卓對于8位數(shù)據(jù)的渲染不正常