android音頻編輯之音頻轉(zhuǎn)換PCM與WAV

前言

本篇開始講解在Android平臺上進行的音頻編輯開發(fā),首先需要對音頻相關(guān)概念有基礎(chǔ)的認識。所以本篇要講解以下內(nèi)容:

  1. 常用音頻格式簡介
  2. WAV和PCM的區(qū)別和聯(lián)系
  3. WAV文件頭信息
  4. 采樣率簡介
  5. 聲道數(shù)和采樣位數(shù)下的PCM編碼
  6. 音頻文件解碼
  7. PCM文件轉(zhuǎn)WAV文件

現(xiàn)在先給出音頻編輯的效果圖,看看能不能提高大家的積極性~,哈哈

音頻裁剪

音頻插入

音頻合成

常用音頻格式簡介

在Android平臺上進行音頻開發(fā),首先需要對常用的音頻格式有個大致的了解。在Android平臺上,常用的音頻格式有:

  • WAV

WAV格式是微軟公司開發(fā)的一種聲音文件格式,也叫波形聲音文件,是最早的數(shù)字音頻格式,被Windows平臺及其應(yīng)用程序廣泛支持。

WAV格式支持許多壓縮算法,支持多種音頻位數(shù)、采樣頻率和聲道,采用44.1kHz的采樣頻率,16位量化位數(shù),因此WAV的音質(zhì)與CD相差無幾,但WAV格式對存儲空間需求太大不便于交流和傳播。

補充:無損格式,缺點:體積十分大!

  • MP3

MP3的全稱是Moving Picture Experts Group Audio Layer III。簡單的說,MP3就是一種音頻壓縮技術(shù),由于這種壓縮方式的全稱叫MPEG Audio Layer3,所以人們把它簡稱為MP3。
MP3是利用 MPEG Audio Layer 3 的技術(shù),將音樂以1:10 甚至 1:12 的壓縮率,壓縮成容量較小的file,換句話說,能夠在音質(zhì)丟失很小的情況下把文件壓縮到更小的程度。而且還非常好的保持了原來的音質(zhì)。

正是因為MP3體積小,音質(zhì)高的特點使得MP3格式幾乎成為網(wǎng)上音樂的代名詞。每分鐘音樂的MP3格式只有1MB左右大小,這樣每首歌的大小只有3-4MB。使用MP3播放器對MP3文件進行實時的解壓縮(解碼),這樣,高品質(zhì)的MP3音樂就播放出來了。

補充:最高比特率320K,高頻部分一刀切是他的缺點。音質(zhì)不高!

  • AMR

全稱Adaptive Multi-Rate 和 Adaptive Multi-Rate Wideband,主要用于移動設(shè)備的音頻,壓縮比比較大,但相對其他的壓縮格式質(zhì)量比較差,多用于人聲,通話,效果還是很不錯的。

  • Ogg

Ogg全稱應(yīng)該是OGG Vobis(ogg Vorbis) 是一種新的音頻壓縮格式,類似于MP3等現(xiàn)有的音樂格式。

但有一點不同的是,它是完全免費、開放和沒有專利限制的。OGG Vobis有一個很出眾的特點,就是支持多聲道,隨著它的流行,以后用隨身聽來聽DTS編碼的多聲道作品將不會是夢想。

Vorbis 是這種音頻壓縮機制的名字,而Ogg則是一個計劃的名字,該計劃意圖設(shè)計一個完全開放性的多媒體系統(tǒng)。目前該計劃只實現(xiàn)了OggVorbis這一部分。

Ogg Vorbis文件的擴展名是.OGG。這種文件的設(shè)計格式是非常先進的。現(xiàn)在創(chuàng)建的OGG文件可以在未來的任何播放器上播放,因此,這種文件格式可以不斷地進行大小和音質(zhì)的改良,而不影響舊有的編碼器或播放器。
補充:目前最好的有損格式之一,MP3部分支持,智能手機裝軟件部分可以支持,最高比特率500kbps。

  • AAC

AAC(Advanced Audio Coding),中文稱為“高級音頻編碼”,出現(xiàn)于1997年,基于 MPEG-2的音頻編碼技術(shù)。

優(yōu)點:相對于mp3,AAC格式的音質(zhì)更佳,文件更小。

不足:AAC屬于有損壓縮的格式,與時下流行的APE、FLAC等無損格式相比音質(zhì)存在“本質(zhì)上”的差距。加之,目前傳輸速度更快的USB3.0和16G以上大容量MP3正在加速普及,也使得AAC頭上“小巧”的光環(huán)不復存在了。

前景:以發(fā)展的眼光來看,正如“高清”正在被越來越多的人所接受一樣,“無損”必定是未來音樂格式的絕對主流。AAC這種“有損”格式的前景不容樂觀

  • FLAC

FLAC即是Free Lossless Audio Codec的縮寫,中文可解為無損音頻壓縮編碼。

FLAC是一套著名的自由音頻壓縮編碼,其特點是無損壓縮。不同于其他有損壓縮編碼如MP3 及 AAC,它不會破任何原有的音頻資訊,所以可以還原音樂光盤音質(zhì)。現(xiàn)在它已被很多軟件及硬件音頻產(chǎn)品所支持。簡而言之,F(xiàn)LAC與MP3相仿,但是是無損壓縮的,也就是說音頻以FLAC方式壓縮不會丟失任何信息。這種壓縮與Zip的方式類似,但是FLAC將給你更大的壓縮比率,因為FLAC是專門針對音頻的特點設(shè)計的壓縮方式,并且你可以使用播放器播放FLAC壓縮的文件,就象通常播放你的MP3文件一樣。

補充:為無損格式,較ape而言,他體積大點,但是兼容性好,編碼速度快,播放器支持更廣。

WAV和PCM的區(qū)別和聯(lián)系

在Android平臺上要進行音頻編輯操作(比如裁剪,插入,合成等),通常都是需要將音頻文件解碼為WAV格式的音頻文件或者PCM文件。那么WAV和PCM之間有什么關(guān)系,這里有必要了解一下。

PCM(Pulse Code Modulation----脈碼調(diào)制錄音)。所謂PCM錄音就是將聲音等模擬信號變成符號化的脈沖列,再予以記錄。PCM信號是由[1]、[0]等符號構(gòu)成的數(shù)字信號,而未經(jīng)過任何編碼和壓縮處理。與模擬信號比,它不易受傳送系統(tǒng)的雜波及失真的影響。動態(tài)范圍寬,可得到音質(zhì)相當好的影響效果。也就是說,PCM就是沒有壓縮的編碼方式,PCM文件就是采用PCM這種沒有壓縮的編碼方式編碼的音頻數(shù)據(jù)文件。

WAV是由微軟開發(fā)的一種音頻格式。WAV符合 PIFF Resource Interchange File Format規(guī)范。所有的WAV都有一個文件頭,這個文件頭音頻流的編碼參數(shù)。WAV對音頻流的編碼沒有硬性規(guī)定,除了PCM之外,還有幾乎所有支持ACM規(guī)范的編碼都可以為WAV的音頻流進行編碼。WAV也可以使用多種音頻編碼來壓縮其音頻流,不過我們常見的都是音頻流被PCM編碼處理的WAV,但這不表示W(wǎng)AV只能使用PCM編碼,MP3編碼同樣也可以運用在WAV中,和AVI一樣,只要安裝好了相應(yīng)的Decode,就可以欣賞這些WAV了。

在Windows平臺下,基于PCM編碼的WAV是被支持得最好的音頻格式,所有音頻軟件都能完美支持,由于本身可以達到較高的音質(zhì)的要求,因此,WAV也是音樂編輯創(chuàng)作的首選格式,適合保存音樂素材。因此,基于PCM編碼的WAV被作為了一種中介的格式,常常使用在其他編碼的相互轉(zhuǎn)換之中,例如MP3轉(zhuǎn)換成WMA。

如上引用的描述,也就是說我們對音頻進行編輯操作,其實就是音頻解碼后的PCM音頻采樣數(shù)據(jù)進行操作,因為PCM記錄的就是采樣后的音頻信息,而我們常說的WAV文件是在PCM數(shù)據(jù)的基礎(chǔ)上添加一組頭信息,用于描述這個WAV文件的采樣率,聲道數(shù),采樣位數(shù),音頻數(shù)據(jù)大小等信息,這樣這個WAV就可以被音頻播放器正確讀取并播放,而單純的PCM文件因為只有編碼的音頻數(shù)據(jù),沒有其他描述信息,所以無法被音頻播放器識別播放。

WAV文件頭信息

接下來有必要了解一下WAV文件頭信息是什么樣的格式信息。

WAV文件頭信息由大小44個字節(jié)的數(shù)據(jù)組成:

  1. 4字節(jié)數(shù)據(jù),內(nèi)容為“RIFF”,表示資源交換文件標識
  2. 4字節(jié)數(shù)據(jù),內(nèi)容為一個整數(shù),表示從下個地址開始到文件尾的總字節(jié)數(shù)
  3. 4字節(jié)數(shù)據(jù),內(nèi)容為“WAVE”,表示W(wǎng)AV文件標識
  4. 4字節(jié)數(shù)據(jù),內(nèi)容為“fmt ”,表示波形格式標識(fmt ),最后一位空格。
  5. 4字節(jié)數(shù)據(jù),內(nèi)容為一個整數(shù),表示PCMWAVEFORMAT的長度
  6. 2字節(jié)數(shù)據(jù),內(nèi)容為一個短整數(shù),表示格式種類(值為1時,表示數(shù)據(jù)為線性PCM編碼)
  7. 2字節(jié)數(shù)據(jù),內(nèi)容為一個短整數(shù),表示通道數(shù),單聲道為1,雙聲道為2
  8. 4字節(jié)數(shù)據(jù),內(nèi)容為一個整數(shù),表示采樣率,比如44100
  9. 4字節(jié)數(shù)據(jù),內(nèi)容為一個整數(shù),表示波形數(shù)據(jù)傳輸速率(每秒平均字節(jié)數(shù)),大小為 采樣率 * 通道數(shù) * 采樣位數(shù)
  10. 2字節(jié)數(shù)據(jù),內(nèi)容為一個短整數(shù),表示DATA數(shù)據(jù)塊長度,大小為 通道數(shù) * 采樣位數(shù)
  11. 2字節(jié)數(shù)據(jù),內(nèi)容為一個短整數(shù),表示采樣位數(shù),即PCM位寬,通常為8位或16位
  12. 4字節(jié)數(shù)據(jù),內(nèi)容為“data”,表示數(shù)據(jù)標記符
  13. 4字節(jié)數(shù)據(jù),內(nèi)容為一個整數(shù),表示接下來聲音數(shù)據(jù)的總大小

由以上信息可知,對于一個PCM文件來說,只要知道它的大小,采樣率,聲道數(shù),采樣位數(shù),就可以通過添加一個WAV文件頭得到一個WAV文件了。

采樣率簡介

那么采樣率是什么意思,我們來了解下。

音頻采樣率是指錄音設(shè)備在一秒鐘內(nèi)對聲音信號的采樣次數(shù),采樣頻率越高聲音的還原就越真實越自然。在當今的主流采集卡上,采樣頻率一般共分為22.05KHz、44.1KHz、48KHz三個等級,22.05KHz只能達到FM廣播的聲音品質(zhì),44.1KHz則是理論上的CD音質(zhì)界限,48KHz則更加精確一些。

在數(shù)字音頻領(lǐng)域,常用的采樣率有:

8,000 Hz - 電話所用采樣率, 對于人的說話已經(jīng)足夠
11,025 Hz

22,050 Hz - 無線電廣播所用采樣率

32,000 Hz - miniDV 數(shù)碼視頻 camcorder、DAT (LP mode)所用采樣率

44,100 Hz - 音頻 CD, 也常用于 MPEG-1 音頻(VCD, SVCD, MP3)所用采樣率

47,250 Hz - 商用 PCM 錄音機所用采樣率

48,000 Hz - miniDV、數(shù)字電視、DVD、DAT、電影和專業(yè)音頻所用的數(shù)字聲音所用采樣率

50,000 Hz - 商用數(shù)字錄音機所用采樣率

96,000 或者 192,000 Hz - DVD-Audio、一些 LPCM DVD 音軌、BD-ROM(藍光盤)音軌、和 HD-DVD (高清晰度 DVD)音軌所用所用采樣率

2.8224 MHz - Direct Stream Digital 的 1 位 sigma-delta modulation 過程所用采樣率。

通常歌曲的采樣率是44100,而Android平臺的人聲錄音支持8000,16000,32000三種采樣率。

聲道數(shù)和采樣位數(shù)下的PCM編碼

接下來再了解下聲道數(shù)和采樣位數(shù)代表什么意思,在PCM編碼中是如何應(yīng)用的。

聲道通常可以分為單聲道和雙聲道,雙聲道又分為左聲道和右聲道。

采樣位數(shù)表示一個采樣數(shù)據(jù)用多少位來表示,通常為8位和16位,對于8位表示一個字節(jié)來表示一個采樣數(shù)據(jù),15位表示用兩個字節(jié)表示一個采樣數(shù)據(jù),兩個字節(jié)為低位字節(jié)和高位字節(jié),通常低位字節(jié)在前,高位字節(jié)在后。

因此結(jié)合聲道和采樣字節(jié)數(shù)(采樣位數(shù)),可以組成下圖的PCM數(shù)據(jù)格式:

PCM編碼格式

可以看到8位單聲道的PCM數(shù)據(jù),只需要一個字節(jié)就能表示一個采樣數(shù)據(jù),而16位雙聲道(立體聲)的PCM數(shù)據(jù),需要4個字節(jié)來表示一個采樣數(shù)據(jù)。那么計算一個PCM大小的方法就很簡單了。

對于8位單聲道,采樣率為8000,1分鐘的PCM音頻來說,大小是

//采樣率 * 通道數(shù) * 采樣位數(shù)/8 * 秒數(shù)
8000 * 1 * 8/8 * 60 = 480000,大約480k

對于16位雙聲道,采樣率為44100,1分鐘的PCM音頻來說,大小是

//采樣率 * 通道數(shù) * 采樣位數(shù)/8 * 秒數(shù)
44100 * 2 * 16/8 * 60 = 10584000,大約10M

而WAV文件的大小就是比PCM多出44個字節(jié)數(shù)。

音頻文件解碼

有了以上音頻相關(guān)知識的了解之后,現(xiàn)在可以來對android上常用音頻文件進行解碼和信息提取了。這里涉及了三個音頻相關(guān)的類:

  • MediaExtractor 媒體文件數(shù)據(jù)提取器,負責媒體文件數(shù)據(jù)的提取操作。
  • MediaFormat 媒體文件格式信息,負責讀取媒體文件的格式(如采樣率,時長,聲道數(shù)等)信息。
  • MediaCodec 媒體文件編解碼類,負責媒體文件數(shù)據(jù)的編解碼操作。

解碼器支持解碼常用的音頻格式,如mp3, wav, 3gpp, 3gp, amr, aac, m4a, ogg, flac等,解碼后的數(shù)據(jù)是PCM編碼的數(shù)據(jù)。下面用代碼實現(xiàn)下如何用上述類實現(xiàn)音頻文件的解碼操作,得到一個PCM數(shù)據(jù)文件

  /**
   * 將音樂文件解碼
   *
   * @param musicFileUrl 源文件路徑
   * @param decodeFileUrl 解碼文件路徑
   * @param startMicroseconds 開始時間 微秒
   * @param endMicroseconds 結(jié)束時間 微秒
   * @param decodeOperateInterface 解碼過程回調(diào)
   */
  private boolean decodeMusicFile(String musicFileUrl, String decodeFileUrl,
      long startMicroseconds, long endMicroseconds, DecodeOperateInterface decodeOperateInterface) {

    //采樣率,聲道數(shù),時長,音頻文件類型
    int sampleRate = 0;
    int channelCount = 0;
    long duration = 0;
    String mime = null;
    
    //MediaExtractor, MediaFormat, MediaCodec
    MediaExtractor mediaExtractor = new MediaExtractor();
    MediaFormat mediaFormat = null;
    MediaCodec mediaCodec = null;

    //給媒體信息提取器設(shè)置源音頻文件路徑
    try {
      mediaExtractor.setDataSource(musicFileUrl);
    }catch (Exception ex){
      ex.printStackTrace();
      try {
        mediaExtractor.setDataSource(new FileInputStream(musicFileUrl).getFD());
      } catch (Exception e) {
        e.printStackTrace();
        LogUtil.e("設(shè)置解碼音頻文件路徑錯誤");
      }
    }

    //獲取音頻格式軌信息
    mediaFormat = mediaExtractor.getTrackFormat(0);

    //從音頻格式軌信息中讀取 采樣率,聲道數(shù),時長,音頻文件類型
    sampleRate = mediaFormat.containsKey(MediaFormat.KEY_SAMPLE_RATE) ? mediaFormat.getInteger(
        MediaFormat.KEY_SAMPLE_RATE) : 44100;
    channelCount = mediaFormat.containsKey(MediaFormat.KEY_CHANNEL_COUNT) ? mediaFormat.getInteger(
        MediaFormat.KEY_CHANNEL_COUNT) : 1;
    duration = mediaFormat.containsKey(MediaFormat.KEY_DURATION) ? mediaFormat.getLong(
        MediaFormat.KEY_DURATION) : 0;
    mime = mediaFormat.containsKey(MediaFormat.KEY_MIME) ? mediaFormat.getString(MediaFormat.KEY_MIME)
            : "";

    LogUtil.i("歌曲信息Track info: mime:"
        + mime
        + " 采樣率sampleRate:"
        + sampleRate
        + " channels:"
        + channelCount
        + " duration:"
        + duration);

    if (TextUtils.isEmpty(mime) || !mime.startsWith("audio/")) {
      LogUtil.e("解碼文件不是音頻文件mime:" + mime);
      return false;
    }

    if (mime.equals("audio/ffmpeg")) {
      mime = "audio/mpeg";
      mediaFormat.setString(MediaFormat.KEY_MIME, mime);
    }

    if (duration <= 0) {
      LogUtil.e("音頻文件duration為" + duration);
      return false;
    }

    //解碼的開始時間和結(jié)束時間
    startMicroseconds = Math.max(startMicroseconds, 0);
    endMicroseconds = endMicroseconds < 0 ? duration : endMicroseconds;
    endMicroseconds = Math.min(endMicroseconds, duration);

    if (startMicroseconds >= endMicroseconds) {
      return false;
    }

    //創(chuàng)建一個解碼器
    try {
      mediaCodec = MediaCodec.createDecoderByType(mime);

      mediaCodec.configure(mediaFormat, null, null, 0);
    } catch (Exception e) {
      LogUtil.e("解碼器configure出錯");
      return false;
    }

    //得到輸出PCM文件的路徑
    decodeFileUrl = decodeFileUrl.substring(0, decodeFileUrl.lastIndexOf("."));
    String pcmFilePath = decodeFileUrl + ".pcm";

    //后續(xù)解碼操作
    getDecodeData(mediaExtractor, mediaCodec, pcmFilePath, sampleRate, channelCount,
        startMicroseconds, endMicroseconds, decodeOperateInterface);

    return true;
  }

以上操作創(chuàng)建了MediaExtractor,獲取MediaFormat用于讀取音頻文件的相關(guān)信息如采樣率,文件類型,聲道數(shù)等。然后創(chuàng)建了MediaCodec用于后續(xù)和MediaExtractor一起進行音頻的解碼操作。接下來看看具體的解碼過程:

  /**
   * 解碼數(shù)據(jù)
   */
  private void getDecodeData(MediaExtractor mediaExtractor, MediaCodec mediaCodec,
      String decodeFileUrl, int sampleRate, int channelCount, final long startMicroseconds,
      final long endMicroseconds, final DecodeOperateInterface decodeOperateInterface) {

    //初始化解碼狀態(tài),未解析完成
    boolean decodeInputEnd = false;
    boolean decodeOutputEnd = false;

    //當前讀取采樣數(shù)據(jù)的大小
    int sampleDataSize;
    //當前輸入數(shù)據(jù)的ByteBuffer序號,當前輸出數(shù)據(jù)的ByteBuffer序號
    int inputBufferIndex;
    int outputBufferIndex;
    //音頻文件的采樣位數(shù)字節(jié)數(shù),= 采樣位數(shù)/8
    int byteNumber;

    //上一次的解碼操作時間,當前解碼操作時間,用于通知回調(diào)接口
    long decodeNoticeTime = System.currentTimeMillis();
    long decodeTime;

    //當前采樣的音頻時間,比如在當前音頻的第40秒的時候
    long presentationTimeUs = 0;

    //定義編解碼的超時時間
    final long timeOutUs = 100;

    //存儲輸入數(shù)據(jù)的ByteBuffer數(shù)組,輸出數(shù)據(jù)的ByteBuffer數(shù)組
    ByteBuffer[] inputBuffers;
    ByteBuffer[] outputBuffers;

    //當前編解碼器操作的 輸入數(shù)據(jù)ByteBuffer 和 輸出數(shù)據(jù)ByteBuffer,可以從targetBuffer中獲取解碼后的PCM數(shù)據(jù)
    ByteBuffer sourceBuffer;
    ByteBuffer targetBuffer;

    //獲取輸出音頻的媒體格式信息
    MediaFormat outputFormat = mediaCodec.getOutputFormat();

    MediaCodec.BufferInfo bufferInfo;

    byteNumber = (outputFormat.containsKey("bit-width") ? outputFormat.getInteger("bit-width") : 0) / 8;

    //開始解碼操作
    mediaCodec.start();

    //獲取存儲輸入數(shù)據(jù)的ByteBuffer數(shù)組,輸出數(shù)據(jù)的ByteBuffer數(shù)組
    inputBuffers = mediaCodec.getInputBuffers();
    outputBuffers = mediaCodec.getOutputBuffers();

    mediaExtractor.selectTrack(0);

    //當前解碼的緩存信息,里面的有效數(shù)據(jù)在offset和offset+size之間
    bufferInfo = new MediaCodec.BufferInfo();

    //獲取解碼后文件的輸出流
    BufferedOutputStream bufferedOutputStream =
        FileFunction.getBufferedOutputStreamFromFile(decodeFileUrl);

    //開始進入循環(huán)解碼操作,判斷讀入源音頻數(shù)據(jù)是否完成,輸出解碼音頻數(shù)據(jù)是否完成
    while (!decodeOutputEnd) {
      if (decodeInputEnd) {
        return;
      }

      decodeTime = System.currentTimeMillis();

      //間隔1秒通知解碼進度
      if (decodeTime - decodeNoticeTime > Constant.OneSecond) {
        final int decodeProgress =
            (int) ((presentationTimeUs - startMicroseconds) * Constant.NormalMaxProgress
                / endMicroseconds);

        if (decodeProgress > 0) {
          notifyProgress(decodeOperateInterface, decodeProgress);
        }

        decodeNoticeTime = decodeTime;
      }

      try {

        //操作解碼輸入數(shù)據(jù)

        //從隊列中獲取當前解碼器處理輸入數(shù)據(jù)的ByteBuffer序號
        inputBufferIndex = mediaCodec.dequeueInputBuffer(timeOutUs);

        if (inputBufferIndex >= 0) {
          //取得當前解碼器處理輸入數(shù)據(jù)的ByteBuffer
          sourceBuffer = inputBuffers[inputBufferIndex];
          //獲取當前ByteBuffer,編解碼器讀取了多少采樣數(shù)據(jù)
          sampleDataSize = mediaExtractor.readSampleData(sourceBuffer, 0);

          //如果當前讀取的采樣數(shù)據(jù)<0,說明已經(jīng)完成了讀取操作
          if (sampleDataSize < 0) {
            decodeInputEnd = true;
            sampleDataSize = 0;
          } else {
            presentationTimeUs = mediaExtractor.getSampleTime();
          }

          //然后將當前ByteBuffer重新加入到隊列中交給編解碼器做下一步讀取操作
          mediaCodec.queueInputBuffer(inputBufferIndex, 0, sampleDataSize, presentationTimeUs,
              decodeInputEnd ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0);

          //前進到下一段采樣數(shù)據(jù)
          if (!decodeInputEnd) {
            mediaExtractor.advance();
          }

        } else {
          //LogUtil.e("inputBufferIndex" + inputBufferIndex);
        }

        //操作解碼輸出數(shù)據(jù)

        //從隊列中獲取當前解碼器處理輸出數(shù)據(jù)的ByteBuffer序號
        outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, timeOutUs);

        if (outputBufferIndex < 0) {
          //輸出ByteBuffer序號<0,可能是輸出緩存變化了,輸出格式信息變化了
          switch (outputBufferIndex) {
            case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
              outputBuffers = mediaCodec.getOutputBuffers();
              LogUtil.e(
                  "MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED [AudioDecoder]output buffers have changed.");
              break;
            case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
              outputFormat = mediaCodec.getOutputFormat();

              sampleRate =
                  outputFormat.containsKey(MediaFormat.KEY_SAMPLE_RATE) ? outputFormat.getInteger(
                      MediaFormat.KEY_SAMPLE_RATE) : sampleRate;
              channelCount =
                  outputFormat.containsKey(MediaFormat.KEY_CHANNEL_COUNT) ? outputFormat.getInteger(
                      MediaFormat.KEY_CHANNEL_COUNT) : channelCount;
              byteNumber =
                  (outputFormat.containsKey("bit-width") ? outputFormat.getInteger("bit-width") : 0)
                      / 8;

              LogUtil.e(
                  "MediaCodec.INFO_OUTPUT_FORMAT_CHANGED [AudioDecoder]output format has changed to "
                      + mediaCodec.getOutputFormat());
              break;
            default:
              //LogUtil.e("error [AudioDecoder] dequeueOutputBuffer returned " + outputBufferIndex);
              break;
          }
          continue;
        }

        //取得當前解碼器處理輸出數(shù)據(jù)的ByteBuffer
        targetBuffer = outputBuffers[outputBufferIndex];

        byte[] sourceByteArray = new byte[bufferInfo.size];

        //將解碼后的targetBuffer中的數(shù)據(jù)復制到sourceByteArray中
        targetBuffer.get(sourceByteArray);
        targetBuffer.clear();

        //釋放當前的輸出緩存
        mediaCodec.releaseOutputBuffer(outputBufferIndex, false);

        //判斷當前是否解碼數(shù)據(jù)全部結(jié)束了
        if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
          decodeOutputEnd = true;
        }

        //sourceByteArray就是最終解碼后的采樣數(shù)據(jù)
        //接下來可以對這些數(shù)據(jù)進行采樣位數(shù),聲道的轉(zhuǎn)換,但這是可選的,默認是和源音頻一樣的聲道和采樣位數(shù)
        if (sourceByteArray.length > 0 && bufferedOutputStream != null) {
          if (presentationTimeUs < startMicroseconds) {
            continue;
          }

          //采樣位數(shù)轉(zhuǎn)換,按自己需要是否實現(xiàn)
          byte[] convertByteNumberByteArray =
              convertByteNumber(byteNumber, Constant.ExportByteNumber, sourceByteArray);

          //聲道轉(zhuǎn)換,按自己需要是否實現(xiàn)
          byte[] resultByteArray = convertChannelNumber(channelCount, Constant.ExportChannelNumber,
              Constant.ExportByteNumber, convertByteNumberByteArray);

          //將解碼后的PCM數(shù)據(jù)寫入到PCM文件
          try {
            bufferedOutputStream.write(resultByteArray);
          } catch (Exception e) {
            LogUtil.e("輸出解壓音頻數(shù)據(jù)異常" + e);
          }
        }

        if (presentationTimeUs > endMicroseconds) {
          break;
        }
      } catch (Exception e) {
        LogUtil.e("getDecodeData異常" + e);
      }
    }

    if (bufferedOutputStream != null) {
      try {
        bufferedOutputStream.close();
      } catch (IOException e) {
        LogUtil.e("關(guān)閉bufferedOutputStream異常" + e);
      }
    }

    //重置采樣率,按自己需要是否實現(xiàn)
    if (sampleRate != Constant.ExportSampleRate) {
      Resample(sampleRate, decodeFileUrl);
    }

    notifyProgress(decodeOperateInterface, 100);

    //釋放mediaCodec 和 mediaExtractor
    if (mediaCodec != null) {
      mediaCodec.stop();
      mediaCodec.release();
    }

    if (mediaExtractor != null) {
      mediaExtractor.release();
    }
  }

以上操作是在一個循環(huán)中,不斷取得源音頻輸入數(shù)據(jù),加入到輸入隊列中,交給MediaCodec處理,然后再從解碼后的輸出隊列中取得輸出數(shù)據(jù),寫入到文件中,其中要判斷源音頻輸入數(shù)據(jù)是否讀取完畢,解碼后的輸出數(shù)據(jù)是否完成,來終止這個循環(huán)。后續(xù)的采樣位數(shù)轉(zhuǎn)換,聲道數(shù)轉(zhuǎn)換,以及采樣率轉(zhuǎn)換都是可選的,不是必須的,默認不實現(xiàn)的話,輸出的PCM數(shù)據(jù)和源音頻是一樣的采樣位數(shù),聲道數(shù),和采樣率。

PCM文件轉(zhuǎn)WAV文件

現(xiàn)在我們得到了解碼后的PCM文件,但是它是不可直接播放的,因為不帶音頻相關(guān)的格式信息,下面我們將PCM和指定的音頻相關(guān)格式信息去轉(zhuǎn)換得到一個可播放的WAV文件:

  /**
   * PCM文件轉(zhuǎn)WAV文件
   * @param inPcmFilePath 輸入PCM文件路徑
   * @param outWavFilePath 輸出WAV文件路徑
   * @param sampleRate 采樣率,例如44100
   * @param channels 聲道數(shù) 單聲道:1或雙聲道:2
   * @param bitNum 采樣位數(shù),8或16
   */
  public static void convertPcm2Wav(String inPcmFilePath, String outWavFilePath, int sampleRate,
      int channels, int bitNum) {

    FileInputStream in = null;
    FileOutputStream out = null;
    byte[] data = new byte[1024];

    try {
      //采樣字節(jié)byte率
      long byteRate = sampleRate * channels * bitNum / 8;
      
      in = new FileInputStream(inPcmFilePath);
      out = new FileOutputStream(outWavFilePath);

      //PCM文件大小
      long totalAudioLen = in.getChannel().size();
      
      //總大小,由于不包括RIFF和WAV,所以是44 - 8 = 36,在加上PCM文件大小
      long totalDataLen = totalAudioLen + 36;

      writeWaveFileHeader(out, totalAudioLen, totalDataLen, sampleRate, channels, byteRate);

      int length = 0;
      while ((length = in.read(data)) > 0) {
        out.write(data, 0, length);
      }
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      if (in != null) {
        try {
          in.close();
        } catch (IOException e) {
          e.printStackTrace();
        }
      }
      if (out != null) {
        try {
          out.close();
        } catch (IOException e) {
          e.printStackTrace();
        }
      }
    }
  }

  
  /**
   * 輸出WAV文件
   * @param out WAV輸出文件流
   * @param totalAudioLen 整個音頻PCM數(shù)據(jù)大小
   * @param totalDataLen 整個數(shù)據(jù)大小
   * @param sampleRate 采樣率
   * @param channels 聲道數(shù)
   * @param byteRate 采樣字節(jié)byte率
   * @throws IOException
   */
  private static void writeWaveFileHeader(FileOutputStream out, long totalAudioLen,
      long totalDataLen, int sampleRate, int channels, long byteRate) throws IOException {
    byte[] header = new byte[44];
    header[0] = 'R'; // RIFF
    header[1] = 'I';
    header[2] = 'F';
    header[3] = 'F';
    header[4] = (byte) (totalDataLen & 0xff);//數(shù)據(jù)大小
    header[5] = (byte) ((totalDataLen >> 8) & 0xff);
    header[6] = (byte) ((totalDataLen >> 16) & 0xff);
    header[7] = (byte) ((totalDataLen >> 24) & 0xff);
    header[8] = 'W';//WAVE
    header[9] = 'A';
    header[10] = 'V';
    header[11] = 'E';
    //FMT Chunk
    header[12] = 'f'; // 'fmt '
    header[13] = 'm';
    header[14] = 't';
    header[15] = ' ';//過渡字節(jié)
    //數(shù)據(jù)大小
    header[16] = 16; // 4 bytes: size of 'fmt ' chunk
    header[17] = 0;
    header[18] = 0;
    header[19] = 0;
    //編碼方式 10H為PCM編碼格式
    header[20] = 1; // format = 1
    header[21] = 0;
    //通道數(shù)
    header[22] = (byte) channels;
    header[23] = 0;
    //采樣率,每個通道的播放速度
    header[24] = (byte) (sampleRate & 0xff);
    header[25] = (byte) ((sampleRate >> 8) & 0xff);
    header[26] = (byte) ((sampleRate >> 16) & 0xff);
    header[27] = (byte) ((sampleRate >> 24) & 0xff);
    //音頻數(shù)據(jù)傳送速率,采樣率*通道數(shù)*采樣深度/8
    header[28] = (byte) (byteRate & 0xff);
    header[29] = (byte) ((byteRate >> 8) & 0xff);
    header[30] = (byte) ((byteRate >> 16) & 0xff);
    header[31] = (byte) ((byteRate >> 24) & 0xff);
    // 確定系統(tǒng)一次要處理多少個這樣字節(jié)的數(shù)據(jù),確定緩沖區(qū),通道數(shù)*采樣位數(shù)
    header[32] = (byte) (channels * 16 / 8);
    header[33] = 0;
    //每個樣本的數(shù)據(jù)位數(shù)
    header[34] = 16;
    header[35] = 0;
    //Data chunk
    header[36] = 'd';//data
    header[37] = 'a';
    header[38] = 't';
    header[39] = 'a';
    header[40] = (byte) (totalAudioLen & 0xff);
    header[41] = (byte) ((totalAudioLen >> 8) & 0xff);
    header[42] = (byte) ((totalAudioLen >> 16) & 0xff);
    header[43] = (byte) ((totalAudioLen >> 24) & 0xff);
    out.write(header, 0, 44);
  }

上面操作其實也很簡單,只要你知道了WAV文件頭信息的格式,將采樣率,聲道數(shù),采樣位數(shù),PCM音頻數(shù)據(jù)大小等信息填充進去,然后將這個44個字節(jié)數(shù)據(jù)拼接到PCM文件的開頭,就得到了一個可播放的WAV文件了。

總結(jié)

上文講解了常用音頻文件的格式,采樣率,聲道,采樣位數(shù)概念,以及PCM數(shù)據(jù)是如何構(gòu)成等內(nèi)容。然后是如何從音頻文件解碼為PCM數(shù)據(jù)文件,以及得到PCM編碼的WAV文件,有了以上的理解后,后續(xù)進行音頻文件的裁剪,插入,合成等編輯操作就更容易理解了。請繼續(xù)關(guān)注后續(xù)的音頻編輯操作處理。

需要源碼的親們請關(guān)注我的微信公眾號【hesong】,那里有源碼地址。創(chuàng)作不易,前進的路上需要親們的支持哦。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,527評論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,687評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,640評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,957評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,682評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,011評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,009評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,183評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,714評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,435評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,665評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,148評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,838評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,251評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,588評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,379評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,627評論 2 380

推薦閱讀更多精彩內(nèi)容

  • 要在計算機內(nèi)播放或是處理音頻文件,也就是要對聲音文件進行數(shù)、模轉(zhuǎn)換,這個過程同樣由采樣和量化構(gòu)成,人耳所能聽到的聲...
    Viking_Den閱讀 10,284評論 1 10
  • 前言 說到視頻,大家自己腦子里基本都會想起電影、電視劇、在線視頻等等,也會想起一些視頻格式 AVI、MP4、RMV...
    ForestSen閱讀 23,160評論 10 203
  • 作者:鄭童宇GitHub:https://github.com/CrazyZty 1.前言 音頻合成在現(xiàn)實生活中應(yīng)...
    鄭童宇閱讀 10,085評論 17 57
  • 最近在折騰itunes音頻處理(裁剪編輯,格式轉(zhuǎn)換,波形繪制) 音頻的一些概念 對每個音頻文件有兩部分:1是文件格...
    osbornZ閱讀 2,188評論 1 5
  • 1.簡單原型鏈 優(yōu)點:簡單,易于實現(xiàn)缺點: 修改sub1.arr后sub2.arr也變了,因為來自原型對象的引用屬...
    sdcV閱讀 291評論 0 0