Android MediaCodec硬解碼AAC音訊檔案(實時AAC音訊幀)並播放

Black_Hao發表於2017-05-22

轉載請註明出處:http://blog.csdn.net/a512337862/article/details/72629755

今天在這裡簡單介紹一下,如何利用android MediaCodec解碼AAC音訊檔案或者實時AAC音訊幀並通過AudioTrack來播放。主要的思路就是從檔案或者網路獲取一幀幀的AAC的資料,送入解碼器解碼後播放。

封裝AudioTrack

AudioTrack主要是用來進行主要是用來播放聲音的,但是隻能播放PCM格式的音訊流。這裡主要是簡單的對AudioTrack進行了封裝,加入了一些異常判斷:

/**
 * Created by ZhangHao on 2017/5/10.
 * 播放pcm資料
 */
public class MyAudioTrack {
    private int mFrequency;// 取樣率
    private int mChannel;// 聲道
    private int mSampBit;// 取樣精度
    private AudioTrack mAudioTrack;

    public MyAudioTrack(int frequency, int channel, int sampbit) {
        this.mFrequency = frequency;
        this.mChannel = channel;
        this.mSampBit = sampbit;
    }

    /**
     * 初始化
     */
    public void init() {
        if (mAudioTrack != null) {
            release();
        }
        // 獲得構建物件的最小緩衝區大小
        int minBufSize = getMinBufferSize();
        mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
                mFrequency, mChannel, mSampBit, minBufSize, AudioTrack.MODE_STREAM);
        mAudioTrack.play();
    }

    /**
     * 釋放資源
     */
    public void release() {
        if (mAudioTrack != null) {
            mAudioTrack.stop();
            mAudioTrack.release();
        }
    }

    /**
     * 將解碼後的pcm資料寫入audioTrack播放
     *
     * @param data   資料
     * @param offset 偏移
     * @param length 需要播放的長度
     */
    public void playAudioTrack(byte[] data, int offset, int length) {
        if (data == null || data.length == 0) {
            return;
        }
        try {
            mAudioTrack.write(data, offset, length);
        } catch (Exception e) {
            Log.e("MyAudioTrack", "AudioTrack Exception : " + e.toString());
        }
    }

    public int getMinBufferSize() {
        return AudioTrack.getMinBufferSize(mFrequency,
                mChannel, mSampBit);
    }
}

這裡簡單介紹一下,在AudioTrack構造方法AudioTrack(int streamType, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes, int mode)裡幾個變數的含義:
1.streamType:指定流的型別,主要包括以下幾種:
- STREAM_ALARM:警告聲
- STREAM_MUSCI:音樂聲
- STREAM_RING:鈴聲
- STREAM_SYSTEM:系統聲音
- STREAM_VOCIE_CALL:電話聲音
因為android系統對不同的聲音的管理是分開的,所以這個引數的作用就是設定AudioTrack播放的聲音型別。

2.sampleRateInHz : 取樣率

3.channelConfig : 聲道

4.audioFormat : 取樣精度

5.bufferSizeInBytes :緩衝區大小,可以通過AudioTrack.getMinBufferSize(int sampleRateInHz, int channelConfig, int audioFormat)來獲取

6.mode : MODE_STATIC和MODE_STREAM:
- MODE_STATIC : 直接把所有的資料載入到快取區,不需要多次write,一般用於佔用記憶體小,延時要求高的情況
- MODE_STREAM : 需要多次write,一般用於像從網路獲取資料或者實時解碼的情況,本次的例子就是這種情況。

我這裡只是簡單的介紹,大家可以去網上找更為詳細的介紹。

AAC解碼器

這裡主要對MediaCodec進行封裝,實現一幀幀去解碼AAC。

/**
 * Created by ZhangHao on 2017/5/17.
 * 用於aac音訊解碼
 */

public class AACDecoderUtil {
    private static final String TAG = "AACDecoderUtil";
    //聲道數
    private static final int KEY_CHANNEL_COUNT = 2;
    //取樣率
    private static final int KEY_SAMPLE_RATE = 48000;
    //用於播放解碼後的pcm
    private MyAudioTrack mPlayer;
    //解碼器
    private MediaCodec mDecoder;
    //用來記錄解碼失敗的幀數
    private int count = 0;

    /**
     * 初始化所有變數
     */
    public void start() {
        prepare();
    }

    /**
     * 初始化解碼器
     *
     * @return 初始化失敗返回false,成功返回true
     */
    public boolean prepare() {
        // 初始化AudioTrack
        mPlayer = new MyAudioTrack(KEY_SAMPLE_RATE, AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_16BIT);
        mPlayer.init();
        try {
            //需要解碼資料的型別
            String mine = "audio/mp4a-latm";
            //初始化解碼器
            mDecoder = MediaCodec.createDecoderByType(mine);
            //MediaFormat用於描述音視訊資料的相關引數
            MediaFormat mediaFormat = new MediaFormat();
            //資料型別
            mediaFormat.setString(MediaFormat.KEY_MIME, mine);
            //聲道個數
            mediaFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, KEY_CHANNEL_COUNT);
            //取樣率
            mediaFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, KEY_SAMPLE_RATE);
            //位元率
            mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 128000);
            //用來標記AAC是否有adts頭,1->有
            mediaFormat.setInteger(MediaFormat.KEY_IS_ADTS, 1);
            //用來標記aac的型別
            mediaFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
            //ByteBuffer key(暫時不瞭解該引數的含義,但必須設定)
            byte[] data = new byte[]{(byte) 0x11, (byte) 0x90};
            ByteBuffer csd_0 = ByteBuffer.wrap(data);
            mediaFormat.setByteBuffer("csd-0", csd_0);
            //解碼器配置
            mDecoder.configure(mediaFormat, null, null, 0);
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }
        if (mDecoder == null) {
            return false;
        }
        mDecoder.start();
        return true;
    }

    /**
     * aac解碼+播放
     */
    public void decode(byte[] buf, int offset, int length) {
        //輸入ByteBuffer
        ByteBuffer[] codecInputBuffers = mDecoder.getInputBuffers();
        //輸出ByteBuffer
        ByteBuffer[] codecOutputBuffers = mDecoder.getOutputBuffers();
        //等待時間,0->不等待,-1->一直等待
        long kTimeOutUs = 0;
        try {
            //返回一個包含有效資料的input buffer的index,-1->不存在
            int inputBufIndex = mDecoder.dequeueInputBuffer(kTimeOutUs);
            if (inputBufIndex >= 0) {
                //獲取當前的ByteBuffer
                ByteBuffer dstBuf = codecInputBuffers[inputBufIndex];
                //清空ByteBuffer
                dstBuf.clear();
                //填充資料
                dstBuf.put(buf, offset, length);
                //將指定index的input buffer提交給解碼器
                mDecoder.queueInputBuffer(inputBufIndex, 0, length, 0, 0);
            }
            //編解碼器緩衝區
            MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
            //返回一個output buffer的index,-1->不存在
            int outputBufferIndex = mDecoder.dequeueOutputBuffer(info, kTimeOutUs);

            if (outputBufferIndex < 0) {
                //記錄解碼失敗的次數
                count++;
            }
            ByteBuffer outputBuffer;
            while (outputBufferIndex >= 0) {
                //獲取解碼後的ByteBuffer
                outputBuffer = codecOutputBuffers[outputBufferIndex];
                //用來儲存解碼後的資料
                byte[] outData = new byte[info.size];
                outputBuffer.get(outData);
                //清空快取
                outputBuffer.clear();
                //播放解碼後的資料
                mPlayer.playAudioTrack(outData, 0, info.size);
                //釋放已經解碼的buffer
                mDecoder.releaseOutputBuffer(outputBufferIndex, false);
                //解碼未解完的資料
                outputBufferIndex = mDecoder.dequeueOutputBuffer(info, kTimeOutUs);
            }
        } catch (Exception e) {
            Log.e(TAG, e.toString());
            e.printStackTrace();
        }
    }

    //返回解碼失敗的次數
    public int getCount() {
        return count;
    }

    /**
     * 釋放資源
     */
    public void stop() {
        try {
            if (mPlayer != null) {
                mPlayer.release();
                mPlayer = null;
            }
            if (mDecoder != null) {
                mDecoder.stop();
                mDecoder.release();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

其實這裡和我之前利用MediaCodec解碼H264很類似,主要就是在因為解碼資料型別不同,所以初始化時有區別。還有一點就是解碼H624時,直接將解碼後資料利用surface顯示,而解碼aac是將解碼後的資料取出來,再利用AudioTrack播放。

讀取aac檔案

這裡是利用執行緒讀aac檔案,獲得一幀幀的aac幀資料,然後送入解碼器播放。

/**
 * Created by ZhangHao on 2017/4/18.
 * 播放aac音訊檔案
 */
public class ReadAACFileThread extends Thread {

    //音訊解碼器
    private AACDecoderUtil audioUtil;
    //檔案路徑
    private String filePath;
    //檔案讀取完成標識
    private boolean isFinish = false;
    //這個值用於找到第一個幀頭後,繼續尋找第二個幀頭,如果解碼失敗可以嘗試縮小這個值
    private int FRAME_MIN_LEN = 50;
    //一般AAC幀大小不超過200k,如果解碼失敗可以嘗試增大這個值
    private static int FRAME_MAX_LEN = 100 * 1024;
    //根據幀率獲取的解碼每幀需要休眠的時間,根據實際幀率進行操作
    private int PRE_FRAME_TIME = 1000 / 50;
    //記錄獲取的幀數
    private int count = 0;

    public ReadAACFileThread(String path) {
        this.audioUtil = new AACDecoderUtil();
        this.filePath = path;
        this.audioUtil.start();
    }

    @Override
    public void run() {
        super.run();
        File file = new File(filePath);
        //判斷檔案是否存在
        if (file.exists()) {
            try {
                FileInputStream fis = new FileInputStream(file);
                //儲存完整資料幀
                byte[] frame = new byte[FRAME_MAX_LEN];
                //當前幀長度
                int frameLen = 0;
                //每次從檔案讀取的資料
                byte[] readData = new byte[10 * 1024];
                //開始時間
                long startTime = System.currentTimeMillis();
                //迴圈讀取資料
                while (!isFinish) {
                    if (fis.available() > 0) {
                        int readLen = fis.read(readData);
                        //當前長度小於最大值
                        if (frameLen + readLen < FRAME_MAX_LEN) {
                            //將readData拷貝到frame
                            System.arraycopy(readData, 0, frame, frameLen, readLen);
                            //修改frameLen
                            frameLen += readLen;
                            //尋找第一個幀頭
                            int headFirstIndex = findHead(frame, 0, frameLen);
                            while (headFirstIndex >= 0 && isHead(frame, headFirstIndex)) {
                                //尋找第二個幀頭
                                int headSecondIndex = findHead(frame, headFirstIndex + FRAME_MIN_LEN, frameLen);
                                //如果第二個幀頭存在,則兩個幀頭之間的就是一幀完整的資料
                                if (headSecondIndex > 0 && isHead(frame, headSecondIndex)) {
                                    //視訊解碼
                                    count++;
                                    Log.e("ReadAACFileThread", "Length : " + (headSecondIndex - headFirstIndex));
                                    audioUtil.decode(frame, headFirstIndex, headSecondIndex - headFirstIndex);
                                    //擷取headSecondIndex之後到frame的有效資料,並放到frame最前面
                                    byte[] temp = Arrays.copyOfRange(frame, headSecondIndex, frameLen);
                                    System.arraycopy(temp, 0, frame, 0, temp.length);
                                    //修改frameLen的值
                                    frameLen = temp.length;
                                    //執行緒休眠
                                    sleepThread(startTime, System.currentTimeMillis());
                                    //重置開始時間
                                    startTime = System.currentTimeMillis();
                                    //繼續尋找資料幀
                                    headFirstIndex = findHead(frame, 0, frameLen);
                                } else {
                                    //找不到第二個幀頭
                                    headFirstIndex = -1;
                                }
                            }
                        } else {
                            //如果長度超過最大值,frameLen置0
                            frameLen = 0;
                        }
                    } else {
                        //檔案讀取結束
                        isFinish = true;
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            Log.e("ReadAACFileThread", "AllCount:" + count + "Error Count : " + audioUtil.getCount());
        } else {
            Log.e("ReadH264FileThread", "File not found");
        }
        audioUtil.stop();
    }

    /**
     * 尋找指定buffer中AAC幀頭的開始位置
     *
     * @param startIndex 開始的位置
     * @param data       資料
     * @param max        需要檢測的最大值
     * @return
     */
    private int findHead(byte[] data, int startIndex, int max) {
        int i;
        for (i = startIndex; i <= max; i++) {
            //發現幀頭
            if (isHead(data, i))
                break;
        }
        //檢測到最大值,未發現幀頭
        if (i == max) {
            i = -1;
        }
        return i;
    }

    /**
     * 判斷aac幀頭
     */
    private boolean isHead(byte[] data, int offset) {
        boolean result = false;
        if (data[offset] == (byte) 0xFF && data[offset + 1] == (byte) 0xF1
                && data[offset + 3] == (byte) 0x80) {
            result = true;
        }
        return result;
    }

    //修眠
    private void sleepThread(long startTime, long endTime) {
        //根據讀檔案和解碼耗時,計算需要休眠的時間
        long time = PRE_FRAME_TIME - (endTime - startTime);
        if (time > 0) {
            try {
                Thread.sleep(time);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

這裡沒有太多的東西,就是通過幀頭來判斷aac幀,並擷取每幀資料送入解碼器。我這裡只是取巧做了簡單的判斷,對幀頭的判斷並不一定滿足所有的aac幀頭,大家可以根據實際的情況自行修改。

結語

1.其實,實現分離音訊幀,利用MediaExtractor這個類就可以實現,但是因為我實際的資料來源是來自網路,所以才會demo才會複雜一點。
2.H264,AAC解碼Demo下載地址:http://download.csdn.net/detail/a512337862/9882200 。附帶aac以及H264檔案以及原始碼

相關文章