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

Android架構發表於2019-03-28

序言

說到在 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 上面,有需要的朋友可以參考一下。

相關文章