AudioTrack播放PCM音頻

一、前言

       說到在 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), 對應着兩種完全不同的使用場景。
       1.MODE_STREAM:在這種模式下,通過 write 一次次把音頻數據寫到 AudioTrack 中。這和平時通過 write 調用往文件中寫數據類似,但這種方式每次都需要把數據從用戶提供的 Buffer 中拷貝到 AudioTrack 內部的 Buffer 中,在一定程度上會使引起延時。爲解決這一問題,AudioTrack 就引入了第二種模式。
       2.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  構造 AudioTrack 實例

public AudioTrack(int streamType, int sampleRateInHz, int channelConfig, int audioFormat,
    int bufferSizeInBytes, int mode) throws IllegalArgumentException {
    this(streamType, sampleRateInHz, channelConfig, audioFormat,
        bufferSizeInBytes, mode, AudioManager.AUDIO_SESSION_ID_GENERATE);
}

public AudioTrack(int streamType, int sampleRateInHz, int channelConfig, int audioFormat,
    int bufferSizeInBytes, int mode) throws IllegalArgumentException {
    this(streamType, sampleRateInHz, channelConfig, audioFormat,
        bufferSizeInBytes, mode, AudioManager.AUDIO_SESSION_ID_GENERATE);
}

public AudioTrack(int streamType, int sampleRateInHz, int channelConfig, int audioFormat,
    int bufferSizeInBytes, int mode, int sessionId) throws IllegalArgumentException {
    // mState already == STATE_UNINITIALIZED
    this((new AudioAttributes.Builder())
            .setLegacyStreamType(streamType)
            .build(),
        (new AudioFormat.Builder())
            .setChannelMask(channelConfig)
            .setEncoding(audioFormat)
            .setSampleRate(sampleRateInHz)
            .build(),
            bufferSizeInBytes,
            mode, sessionId);
    deprecateStreamTypeForPlayback(streamType, "AudioTrack", "AudioTrack()");
}

在採樣 pcm 音頻數據需要設置對應的採樣率,採樣精度,採樣的通道數和採樣的緩衝區大小,如果播放的音頻是使用 AudioRecord 錄製的,那麼這些參數配置信息需要和 AudioRerord一致,不然播放就會出現奇怪的問題。

1.1 AudioTrack 播放音頻時會有兩種方式

音頻播放的方式,有兩種方式 MODE_STATIC 或者 MODE_STREAM 。

  • MODE_STATIC:預先將需要播放的音頻數據讀取到內存中,然後纔開始播放。
  • MODE_STREAM:邊讀邊播,不會將數據直接加載到內存

1.2 MODE_STREAM 的方式構建 AudioTrack 實例

/**
 * 構建 AudioTrack 實例對象
 */
private void createStreamModeAudioTrack() {
    if (audioTrack == null) {
        bufferSize = AudioTrack.getMinBufferSize(44100, 
            AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);
        audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, 44100,     
            AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, bufferSize, 
            AudioTrack.MODE_STREAM);
    }
}

1.3 MODE_STATIC 的方式構建 AudioTrack 實例

/**
 * 構建 AudioTrack 實例對象
 */
private void createStreamModeAudioTrack() {
    if (audioTrack == null) {
    //file 就是需要播放的音頻文件,這裏的buffersize就是文件的大小
        audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
            44100, AudioFormat.CHANNEL_IN_MONO,
            AudioFormat.ENCODING_PCM_16BIT, (int) file.length(), AudioTrack.MODE_STATIC);
    }
}

2 寫入數據

不間斷通過write方法的寫數據給AudioTrack。

注意: 對於 MODE_STREAM 寫入數據,會阻塞,直到寫入的數據都傳輸給 AudioTrack。
            對於 MODE_STATIC 會將數據拷貝到緩衝區中,並在該方法返回後執行 play() 方法播放音頻數據。

  • int write (byte[] audioData, int offsetInBytes,int sizeInBytes)
  • int write (short[] audioData, int offsetInShorts, int sizeInShorts)
  • int write (float[] audioData, int offsetInFloats, int sizeInFloats,int writeMode)

該方法的返回值:
正確:>=0 該值表示寫入的數據量。
錯誤:<0

  • ERROR_INVALID_OPERATION
  • ERROR_BAD_VALUE
  • ERROR_DEAD_OBJECT
  • ERROR

2.1 兩種方式寫入數據的區別

  • MODE_STATIC

       在 AudioTrack 創建之處,會初始化一個與其相關聯的 buffer 緩衝區,這個緩衝區的大小是在構造方法指定的。這個大小表示 AudioTrack 可以播放多久。對於 MODE_STATIC 這種模式下,這個 buffer 的大小就是需要播放的文件或者流的大小。

//寫入數據大小 array 就是預先將音頻數據加載到array數組中
int writeResult = audioTrack.write(array, 0, array.length);
//檢查寫入的結果,如果是異常情況,則直接需要釋放資源
if (writeResult == AudioTrack.ERROR_INVALID_OPERATION || writeResult == AudioTrack.ERROR_BAD_VALUE
    || writeResult == AudioTrack.ERROR_DEAD_OBJECT || writeResult == AudioTrack.ERROR) {
    //出異常情況
    isPlaying = false;
    release();
    return;
}
  • MODE_STREAM

       使用這種方式是通過將數據寫入到緩衝區中,而需要注意寫入到這個緩衝區的數據大小,需要確保小於或者等於這個構造 AudioTrack 的緩衝區大小。
       AudioTrack 不是 final 類型,也就是說可以使用繼承實現自己的功能,但是官方文檔表示不建議這樣做。

 //邊讀邊播
 byte[] buffer = new byte[bufferSize];
 while (fis.available() > 0) {
     int readCount = fis.read(buffer);
     if (readCount == -1) {
         Log.e(TAG, "沒有更多數據可以讀取了");
         break;
     }
     int writeResult = audioTrack.write(buffer, 0, readCount);
     if (writeResult >= 0) {
         //success
     } else {
         //fail
         //丟掉這一塊數據
         continue;
     }
 }

       這個緩衝區大小可以通過 AudioTrack.getMinBufferSize 來獲取

bufferSize = AudioTrack.getMinBufferSize(44000, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);

3 狀態判斷

3.1 AudioTrack 狀態判斷

檢測一個已經創建好的 AudioTrack 的狀態,確保操作在正確初始化之後進行。當需要進行播放前,校驗 AudioTrack 是否處於正確的狀態。

/**
 * State of an AudioTrack that was not successfully initialized upon creation.
 */
public static final int STATE_UNINITIALIZED = 0;
/**
 * State of an AudioTrack that is ready to be used.
 */
public static final int STATE_INITIALIZED   = 1;
/**
 * State of a successfully initialized AudioTrack that uses static data,
 * but that hasn't received that data yet.
 */
public static final int STATE_NO_STATIC_DATA = 2;

/**
 * Returns the state of the AudioTrack instance. This is useful after the
 * AudioTrack instance has been created to check if it was initialized
 * properly. This ensures that the appropriate resources have been acquired.
 * @see #STATE_UNINITIALIZED   表示 AudioTrack 創建時沒有成功地初始化
 * @see #STATE_INITIALIZED     表示 AudioTrack 已經是可以使用了
 * @see #STATE_NO_STATIC_DATA  表示當前是使用 MODE_STATIC ,但是還沒往緩衝區中寫入數據。
 *                             當接收數據之後會變爲 STATE_INITIALIZED 狀態                
 */
public int getState() {
    return mState;
}
//播放時,狀態校驗
if (audioTrack.getState() != AudioTrack.STATE_INITIALIZED) {
    Log.e(TAG, "不能播放,當前播放器未處於初始化狀態..");
    return;
}

3.2 AudioTrack 播放狀態

/** indicates AudioTrack state is stopped */
public static final int PLAYSTATE_STOPPED = 1;  // matches SL_PLAYSTATE_STOPPED
/** indicates AudioTrack state is paused */
public static final int PLAYSTATE_PAUSED  = 2;  // matches SL_PLAYSTATE_PAUSED
/** indicates AudioTrack state is playing */
public static final int PLAYSTATE_PLAYING = 3;  // matches SL_PLAYSTATE_PLAYING

/**
 * Returns the playback state of the AudioTrack instance.
 * @see #PLAYSTATE_STOPPED  停止
 * @see #PLAYSTATE_PAUSED   暫停
 * @see #PLAYSTATE_PLAYING  正在播放
 */
public int getPlayState() {
    synchronized (mPlayStateLock) {
        return mPlayState;
    }
}

4 播放 play

  • 對於 MODE_STATIC 模式,必須要調用 write(...) 相關方法將數據寫入到對應的緩衝區中,然後纔可以調用 paly(...) 方法進行播放操作。
//先將所有的數據寫入到緩衝區
write(...)
//然後在播放
play(...)
  • 對於 MODE_STREAM 模式,必須要調用 play(...) 方法,然後調用 write(...) 方法進行寫入數據播放操作。
paly(...)

new Thread() {
    public void run() {
        //一系列的 write 操作
        write(...)
    }
}.start();
/**
 * 播放,使用stream模式
 */
private void playInModeStream() {
    final int minBufferSize = AudioTrack.getMinBufferSize(44100, AudioFormat.CHANNEL_IN_MONO,
            AudioFormat.ENCODING_PCM_16BIT);
    audioTrack = new AudioTrack(
            new AudioAttributes.Builder()
                    .setUsage(AudioAttributes.USAGE_MEDIA)
                    .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
                    .build(),
            new AudioFormat.Builder().setSampleRate(SAMPLE_RATE_INHZ)
                    .setEncoding(AUDIO_FORMAT)
                    .setChannelMask(channelConfig)
                    .build(),
            minBufferSize,
            AudioTrack.MODE_STREAM,
            AudioManager.AUDIO_SESSION_ID_GENERATE);
    audioTrack.play();

    File file = new File(getExternalFilesDir(Environment.DIRECTORY_MUSIC), "test.pcm");
    try {
        fileInputStream = new FileInputStream(file);
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    byte[] tempBuffer = new byte[minBufferSize];
                    while (fileInputStream.available() > 0) {
                        int readCount = fileInputStream.read(tempBuffer);
                        if (readCount == AudioTrack.ERROR_INVALID_OPERATION ||
                                readCount == AudioTrack.ERROR_BAD_VALUE) {
                            continue;
                        }
                        if (readCount != 0 && readCount != -1) {
                            audioTrack.write(tempBuffer, 0, readCount);
                        }
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

5 AudioTrack 狀態

5.1 停止播放

/**
 * Stops playing the audio data.
 * When used on an instance created in {@link #MODE_STREAM} mode, audio will stop playing
 * after the last buffer that was written has been played. For an immediate stop, use
 * {@link #pause()}, followed by {@link #flush()} to discard audio data that hasn't been played
 * back yet.
 * 對於 MODE_STREAM 模式,如果單是調用 stop 方法, AudioTrack 會等待緩衝的最後一幀數據播放完畢之
 * 後,纔會停止,如果需要立即停止,那麼就需要調用 pause 然後調用 flush 這兩個方法,那麼 
 * AudioTrack 就是丟緩衝區中剩餘的數據。
 * @throws IllegalStateException
 */
public void stop() throws IllegalStateException {
    ...
}

5.2 暫停

/**
 * Pauses the playback of the audio data. Data that has not been played
 * back will not be discarded. Subsequent calls to {@link #play} will play
 * this data back. See {@link #flush()} to discard this data.
 * 暫停播放,但是緩衝區中沒有被播放的數據不會被捨棄,調用 play 方法即可接着播放。
 * @throws IllegalStateException
 */
public void pause() throws IllegalStateException {
    ...
}

5.3 刷新

/**
 * 刷新正在排隊播放的音頻數據,調用該方法會將寫入到緩衝區但沒有被播放的音頻數據都會被丟棄。
 * 如果是非 STREAM 或者沒有執行 pasuse 或者 stop 將不會有任何效果。
 */
public void flush() {
    ...
}

5.4 釋放

/**
 * Releases the native AudioTrack resources.
 * 釋放本地 AudioTrack 資源
 */
public void release() {
    ...
}

5.5 示例代碼

public void stop() { 
    if ((audioTrack != null) && (audioTrack.getState() == AudioTrack.STATE_INITIALIZED)) {
        if (audioTrack.getPlayState() != AudioTrack.PLAYSTATE_STOPPED) {
            audioTrack.flush();
            audioTrack.stop();
        }
    }
} 

三、 AudioTrack 和 MediaPlayer 的對比

播放聲音可以用MediaPlayer和AudioTrack,兩者都提供了Java API供應用開發者使用。雖然都可以播放聲音,但兩者還是有很大的區別的。

6.1 區別

其中最大的區別是MediaPlayer可以播放多種格式的聲音文件,例如MP3,AAC,WAV,OGG,MIDI等。MediaPlayer會在framework層創建對應的音頻解碼器。而AudioTrack只能播放已經解碼的PCM流,如果對比支持的文件格式的話則是AudioTrack只支持wav格式的音頻文件,因爲wav格式的音頻文件大部分都是PCM流。AudioTrack不創建解碼器,所以只能播放不需要解碼的wav文件。

6.2 聯繫

MediaPlayer在framework層還是會創建AudioTrack,把解碼後的PCM數流傳遞給AudioTrack,AudioTrack再傳遞給AudioFlinger進行混音,然後才傳遞給硬件播放,所以是MediaPlayer包含了AudioTrack。

每一個音頻流對應着一個AudioTrack類的一個實例,每個AudioTrack會在創建時註冊到 AudioFlinger中,由AudioFlinger把所有的AudioTrack進行混合(Mixer),然後輸送到AudioHardware中進行播放,目前Android同時最多可以創建32個音頻流,也就是說,Mixer最多會同時處理32個AudioTrack的數據流。

6.3 SoundPool

在接觸Android音頻播放API的時候,發現SoundPool也可以用於播放音頻。下面是三者的使用場景:MediaPlayer 更加適合在後臺長時間播放本地音樂文件或者在線的流式資源; SoundPool 則適合播放比較短的音頻片段,比如遊戲聲音、按鍵聲、鈴聲片段等等,它可以同時播放多個音頻; 而 AudioTrack 則更接近底層,提供了非常強大的控制能力,支持低延遲播放,適合流媒體和VoIP語音電話等場景。

 

參考文章:

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