Android音頻: 如何使用AudioTrack播放一個WAV格式文件?

翻譯 By Long Luo

原文鏈接:Android Audio: Play a WAV file on an AudioTrack

譯者注:
1. 由於這是技術文章,所以有些詞句使用原文,表達更準確。
2. 由於水平有效,有些地方可能翻譯的不夠準確,如有不當之處,敬請批評指正.
3. 針對某些語句,適當補充了上下文及更適合中文閱讀,儘量做到信達雅。

如果你已經成功地瞭解了關於AudioTrack一些話題,那麼你可能享受它帶來的好處,例如低延遲(在STATIC(靜態)模式),能夠生成流式音頻(在STREAM(流)模式)以及在播放之前,就能夠訪問和修改原始聲音數據。

不過,現在的問題是如何從源獲取數據。許多應用需要使用的AudioTrack並不能簡單的生成PCM音頻(一個例子,比如Ethereal Dialpad或者其他類似的App)。你可能需要從文件源去加載數據,例如WAVMP3文件。

不要期望使用MediaPlayer,去解碼WAV文件和MP3音頻。雖然MediaPlayer播放這些文件非常好,但是其播放邏輯完全在Native層,同時並沒有爲我們提供額外選項,允許我們使用其他解碼器實現我們的目的。因此,我們必須從手動地從音頻文件進行解碼出PCM

在這篇文章中,將會討論WAV格式文件。而在下一課中,我們將會更進一步,討論如何從MP3文件讀取音頻。

背景知識: 一些數字音頻術語

如果你的App不是專門爲數字音頻設計,那麼在繼續我們的討論之前,你可能需要先了解一些基本的縮略語。別擔心,都很簡單,我們不需要對此做深入挖掘。

  • PCM(脈衝調製方式) – 實現一個物理音頻信號變成數字化最簡單方法。基本原理就是信號變成了一個數字陣列,而其中每個數字代表的是聲音在特定的時間瞬間的電平也可以說是能量(振幅)。(如果這種解釋在科學上可能不會很準確,那我就只能說聲抱歉了)。信不信由你,你可以使用這種方法表示任何複雜的聲音,而且回放出來也非常精準。在這裏,我們將只會談到線性PCM。在線性PCM中,其中陣列中的每個數字都是原始聲音振幅的線性表示。在某些情況下,對數映射能夠更好地表示原來的聲音幅度比例情況 – 但是我們不會討論那些情況。

  • Sampling rate(採樣率):- 每秒你的數字聲音有多少樣本(聲音幅度用數字表示)。樣本越多,你能得到聲音質量越好。目前在消費類音頻系統目前使用的採樣率通常是22050,44100和48000Hz/s。

  • 每個樣品分辨率/採樣大小/位 – 定義表示振幅數字的大小和格式。例如,如果您使用的是8位整數,你只能表達出256級的幅度,所以原來的物理波形將被簡化爲256個離散電平,與此同時,你將失去一些聲音精度也可以說是質量。如果你使用16位,那麼聲音質量變得更好。事實上,大部分時間你可能會使用16位音頻。其他選項包括24位,32位(這些都是Android現在不支持的),或是使用浮點數。

  • 聲道 – 既可以是單聲道,也可以是立體聲(2個聲道),或者更多聲道(但是Android不支持)。如果你想要有立體聲,你需要有立體聲音頻,就必須要在每個聲道都需要有一個獨立的PCM數組,相應的信息量也會翻倍。

上述定義也有助於你理解特定的格式和長度的音頻緩衝區的數據量,以便提前預備緩衝區。也就是你需要一個緩衝區,以用於存儲5秒長度以44100Hz採樣率的立體聲16-bit線性PCM數據。數據計算公式如下所示:

5 sec * 44100 samples per sec * 2 bytes per sample * 2 channels = 882,000 bytes

這一數額所需的內存可能會讓初學者感到驚訝,因爲當你往你的磁盤上存儲的音頻時,一個MP3文件,一個880KB的文件就可以容納以相同的採樣率和分辨率1分鐘時長的音軌。這是爲什麼呢?因爲先進的格式,比如MP3格式。因爲我們大腦無法分辨識別出一些音頻的內容,所以使用了很多複雜的方式在壓縮的過程中去掉了這些內容。然而,大多數低等級的音頻API,包括Android的AudioTrack只能接受線性PCM。這就是爲什麼如果我們不能把整個樣品都放在內存中,我們需要將要處理的數據流,循環緩衝區和其他聰明的方式來使用音頻API。

希望這樣的解釋並沒有讓你產生困惑,現在讓我們繼續來實際做一些與Android上的數字音頻有關的工作吧!

WAV文件格式

我們的目標是用一個InputStream,由其從一個WAV文件加載PCM數據,來提供原始字節數據。然後我們就可以將原始的PCM數據直接推送到使用已經正確的配置好了的AudioTrack.write,通過使用AudioTrack.write()這個API。

WAV文件包含一個文件頭和具體數據會。我們需要讀取文件頭以知道諸如採樣速率,分辨率等信息。另外,我們通過文件頭,也可以知道此格式是否支持。WAV可以封裝成多種格式,我們無法全部支持。也許,只是合理的採樣率,分辨率和通道的線性PCM格式。

WAV格式的細節在互聯網上都可以找到,你僅僅需要在Google上搜索下。但是,遺憾的是,我並沒有搜索到一個很好的Java庫來讀取WAV文件,而且可以移植到Android下。因此,我自己寫了一些簡單的代碼。

下面這個方法就是如何讀取一個WAV文件的頭部:

private static final String RIFF_HEADER = "RIFF";
private static final String WAVE_HEADER = "WAVE";
private static final String FMT_HEADER = "fmt ";
private static final String DATA_HEADER = "data";

private static final int HEADER_SIZE = 44;

private static final String CHARSET = "ASCII";

/* ... */

public static WavInfo readHeader(InputStream wavStream) throws IOException,
        DecoderException {

    ByteBuffer buffer = ByteBuffer.allocate(HEADER_SIZE);
    buffer.order(ByteOrder.LITTLE_ENDIAN);

    wavStream.read(buffer.array(), buffer.arrayOffset(), buffer.capacity());

    buffer.rewind();
    buffer.position(buffer.position() + 20);
    int format = buffer.getShort();
    checkFormat(format == 1, "Unsupported encoding: " + format); // 1 means
                                                                    // Linear
                                                                    // PCM
    int channels = buffer.getShort();
    checkFormat(channels == 1 || channels == 2, "Unsupported channels: "
            + channels);
    int rate = buffer.getInt();
    checkFormat(rate <= 48000 && rate >= 11025, "Unsupported rate: " + rate);
    buffer.position(buffer.position() + 6);
    int bits = buffer.getShort();
    checkFormat(bits == 16, "Unsupported bits: " + bits);
    int dataSize = 0;
    while (buffer.getInt() != 0x61746164) { // "data" marker
        Log.d(TAG, "Skipping non-data chunk");
        int size = buffer.getInt();
        wavStream.skip(size);

        buffer.rewind();
        wavStream.read(buffer.array(), buffer.arrayOffset(), 8);
        buffer.rewind();
    }
    dataSize = buffer.getInt();
    checkFormat(dataSize > 0, "wrong datasize: " + dataSize);

    return new WavInfo(new FormatSpec(rate, channels == 2), dataSize);
}

上面的代碼中,缺少的部分應該是顯而易見的。正如你所看到的,僅僅支持16位,但在你可以修改代碼以支持8位(AudioTrack不支持任何其他分辨率的)。

下面這個方法,則是用來讀取文件剩餘的部分 – 音頻數據

public static byte[] readWavPcm(WavInfo info, InputStream stream)
        throws IOException {
    byte[] data = new byte[info.getDataSize()];
    stream.read(data, 0, data.length);
    return data;
}

我們讀取的WavInfo結構體,包含採樣率,分辨率和聲道數已經足夠讓我們去播放我們讀取的音頻了。

如果我們不需要將全部音頻數據一次性放入內存中,我們可以使用一個InputStream,一點一點地讀取。

將PCM傳入AudioTrack

我們現在面臨2種情況,新建一個適合這種格式的AudioTrack,或者使用一個已存在的AudioTrack,但是可能和我們WAV音頻數據的格式不一致。

在第一種情況,事情就很簡單了,我們僅僅需要使用AudioTrack構造器構造一個我們已經從WAV頭部對應的即可。

第二種情況,我們就需要將我們的音頻變成AudioTrack需要的目標格式。我們需要做一下幾種轉換方式:

如果採樣率不同,要麼丟棄或複製一個樣本以便和目標速率相匹配。如果分辨率是不同的,將源信號分辨率映射到目標分辨率,從16位到8位,反之亦然。如果信道不同,我們要麼將立體聲聲道混合成一個單聲道或重複單聲道的數據把它變成準立體聲。(請考慮將這些算法的實現放在Native層,因爲Native層在做這類處理有很大的優勢。)

在其他情況下,我們已經確定格式已經匹配。我們使用AudioTrack.write()寫入緩衝區,以便實現回放。

記住,如果你使用靜態模式,你需要在play()之前,新建一個包含準確的緩衝區大小的AudioTrack ,同時寫入write()音頻數據。而在流模式下,我們可以先使用AudioTrackplay(),然後在使用write()寫入數據部分

總結

你想實現AudioTrack上播放WAV音頻可能有很多原因。有時候,可能是SoundPool有尺寸限制,或是MediaPlayer會有延遲和對資源佔用太高,讓你考慮使用這種方式。有時候你需要修改音頻或者混合音頻。不管任何情況,這篇文章試圖告訴你應該如何做。

在下一篇中,我們將會討論MP3音頻,敬請期待:-)

Long Luo for Part 1 created at 23:15 ~ 00: 33 June 21th, 2014 @Shenzhen, China.

Long Luo for Part 2 created at 16:00 ~ 17: 15 June 22th, 2014 @Shenzhen, China.

發佈了84 篇原創文章 · 獲贊 29 · 訪問量 68萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章