AudioTrack播放PCM文件(一)

前言

在開始如何在安卓系統(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ù)的渲染不正常

項目地址

Demo

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容