Android 音視頻開發 - 使用AudioTrack播放音頻

序言

說到在 Android 平臺上播放音頻,我們最先想到的是 MediaPlayer,系統 API 對其做了比較全面的封裝,開發者用少量的代碼就能實現播放功能。MediaPlayer 可以播放多種格式的聲音文件,例如 MP3,AAC,WAV,OGG,MIDI 等,而 AudioTrack 只能播放 PCM 數據流。

實際上,MediaPlayer 在播放音頻時,在 Framework 層還是會創建 AudioTrack,把解碼後的 PCM 數流傳遞給 AudioTrack,最後由 AudioFlinger 進行混音,把音頻傳遞給硬件播放出來。利用 AudioTrack 播放只是跳過 Mediaplayer 的解碼部分而已。如果是實時的音頻數據,那麼只能用 AudioTrack 進行播放。

AudioTrack 有兩種數據加載模式(MODE_STREAM 和 MODE_STATIC), 對應着兩種完全不同的使用場景。

  • MODE_STREAM:在這種模式下,通過 write 一次次把音頻數據寫到 AudioTrack 中。這和平時通過 write 調用往文件中寫數據類似,但這種方式每次都需要把數據從用戶提供的 Buffer 中拷貝到 AudioTrack 內部的 Buffer 中,在一定程度上會使引起延時。爲解決這一問題,AudioTrack 就引入了第二種模式。
  • MODE_STATIC:在這種模式下,只需要在 play 之前通過一次 write 調用,把所有數據傳遞到 AudioTrack 中的內部緩衝區,後續就不必再傳遞數據了。這種模式適用於像鈴聲這種內存佔用較小、延時要求較高的文件。但它也有一個缺點,就是一次 write 的數據不能太多,否則系統無法分配足夠的內存來存儲全部數據。

在 AudioTrack 構造函數中,會接觸到 AudioManager.STREAM_MUSIC 這個參數。它的含義與 Android 系統對音頻流的管理和分類有關。Android 將系統的聲音分爲好幾種流類型,下面是幾個常見的:

  • STREAM_ALARM:警告聲
  • STREAM_MUSIC:音樂聲,例如 music 等
  • STREAM_RING:鈴聲
  • STREAM_SYSTEM:系統聲音,例如低電提示音,鎖屏音等
  • STREAM_VOICE_CALL:通話聲

注意:上面這些類型的劃分和音頻數據本身並沒有關係。例如 MUSIC 和 RING 類型都可以是某首 MP3 歌曲。另外,聲音流類型的選擇沒有固定的標準,例如,鈴聲預覽中的鈴聲可以設置爲 MUSIC 類型。音頻流類型的劃分和 Audio 系統對音頻的管理策略有關。

我們用代碼實踐一下播放的流程

  1. 創建播放對象,參數和 AudioRecord 有相似之處。
    public void createAudioTrack(String filePath) {
        mFilePath = filePath;
        mBufferSizeInBytes = AudioTrack.getMinBufferSize(AUDIO_SAMPLE_RATE, AUDIO_CHANNEL, AUDIO_ENCODING);
        mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, AUDIO_SAMPLE_RATE, AUDIO_CHANNEL, AUDIO_ENCODING,
                mBufferSizeInBytes, AudioTrack.MODE_STREAM);
        mStatus = Status.STATUS_READY;
    }

  1. 開始播放,不斷從文件中讀數據,然後向 Buffer 裏面寫數據。
    public void start() {
        if (mStatus == Status.STATUS_NO_READY || mAudioTrack == null) {
            throw new IllegalStateException("播放器尚未初始化");
        }
        if (mStatus == Status.STATUS_START) {
            throw new IllegalStateException("正在播放...");
        }
        Log.d(TAG, "===start===");
        mExecutorService.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    writeAudioData();
                } catch (IOException e) {
                    Log.e(TAG, e.getMessage());
                    mMainHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            Toast.makeText(mContext, "播放出錯", Toast.LENGTH_SHORT).show();
                        }
                    });
                }
            }
        });
        mStatus = Status.STATUS_START;
    }

    private void writeAudioData() throws IOException {
        DataInputStream dis = null;
        try {
            mMainHandler.post(new Runnable() {
                @Override
                public void run() {
                    Toast.makeText(mContext, "播放開始", Toast.LENGTH_SHORT).show();
                }
            });
            FileInputStream fis = new FileInputStream(mFilePath);
            dis = new DataInputStream(new BufferedInputStream(fis));
            byte[] bytes = new byte[mBufferSizeInBytes];
            int len;
            mAudioTrack.play();
            while ((len = dis.read(bytes)) != -1 && mStatus == Status.STATUS_START) {
                mAudioTrack.write(bytes, 0, len);
            }
            mMainHandler.post(new Runnable() {
                @Override
                public void run() {
                    Toast.makeText(mContext, "播放結束", Toast.LENGTH_SHORT).show();
                }
            });
        } finally {
            if (dis != null) {
                dis.close();
            }
        }
    }

  1. 停止播放,釋放資源。
    public void stop() {
        Log.d(TAG, "===stop===");
        if (mStatus == Status.STATUS_NO_READY || mStatus == Status.STATUS_READY) {
            throw new IllegalStateException("播放尚未開始");
        } else {
            mAudioTrack.stop();
            mStatus = Status.STATUS_STOP;
            release();
        }
    }

    public void release() {
        Log.d(TAG, "==release===");
        if (mAudioTrack != null) {
            mAudioTrack.release();
            mAudioTrack = null;
        }
        mStatus = Status.STATUS_NO_READY;
    }

具體代碼在 GitHub 上面,有需要的朋友可以參考一下。
【附錄】

需要資料的朋友可以加入Android架構交流QQ羣聊:513088520

點擊鏈接加入羣聊【Android移動架構總羣】:加入羣聊

獲取免費學習視頻,學習大綱另外還有像高級UI、性能優化、架構師課程、NDK、混合式開發(ReactNative+Weex)等Android高階開發資料免費分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章