Android視頻編輯器(五)音頻編解碼、從視頻中分離音頻、音頻混音、音頻音量調節等

前言

     這篇博客,主要講解的是android端的音頻處理,在開發Android視頻編輯器的時候,有一個非常重要的點就是音頻的相關處理。比如如何從視頻中分離音頻(保存爲mp3文件),然後分離出來的音頻如何單聲道和雙聲道互轉,還有就是如果把兩個音頻文件合併爲一個音頻文件(音頻混音),以及如何調節音頻的原始大小。那這些功能的運用場景做哪裏呢?比如如果我們想給視頻文件增加要給bgm,那如果保留原聲的情況下,就需要用到音頻的混音,將原聲和bgm合併爲一個音頻文件,然後兩種聲音的相對大小我們肯定也是需要可以調節的,比如是bgm聲音大一點還是原聲音量大一點。這裏還會存在另外一個問題,就是雙聲道和單聲道的音頻不能直接混音,所以我們也需要學習雙聲道和單聲道互轉。其實類似的功能軟件做電腦端已經有很多,而我們這裏就是探究他的實現原理,從而開發出android端的音頻處理功能。下面我們就一一來實現這些功能。
    這篇博客的重點內容包括android平臺的音頻編解碼、音頻的一些基礎知識、歸一化混音算法等。
    本系列的文章包括如下:
       5、android視頻編輯器之音頻編解碼、從視頻中分離音頻、音頻混音、音頻音量調節等
       6、android視頻編輯器之通過OpenGL做不同視頻的拼接
       7、android視頻編輯器之音視頻裁剪、增加背景音樂等

從視頻文件中分離出音頻文件

    首先,我們來實現我們的第一個功能,從視頻文件中把音頻文件分離出來,我們的目標是從一個完整的視頻中,分離出他的音頻並且保存爲aac文件或者mp3文件。首先在android音視頻的處理相關類中,我們需要有兩個很重要的類,一個是音視頻的分離器MediaExtractor 另一個就是音視頻的混合器MediaMuxer。
    
     MediaExtractor:可以從當前的視頻文件中讀取到音視頻相關的信息(音視頻的編碼格式等),並且逐幀讀取文件中的音視頻數據。
     MediaMuxer:可以將編碼後的音視頻數據保存爲一個獨立的文件,可以只寫入音頻數據保存爲音頻文件,也可以同時寫入音頻和視頻數據,保存爲一個有畫面有聲音的視頻文件。

    要實現我們的目標,那我們已經很清楚的知道一個大致的過程了,那就是從MediaExtractor從視頻中分離出音頻數據(編碼好的),通過MediaMuxer,將分離出來的音頻數據保存爲一個音頻文件。

    首先需要初始化一個分離器MediaExtractor,並且給他設置文件,拿到音頻的format和信道
MediaExtractor extractor = new MediaExtractor();
int audioTrack = -1;
boolean hasAudio = false;
try {
     extractor.setDataSource(videoPath);
     for (int i = 0; i < extractor.getTrackCount(); i++) {
          MediaFormat trackFormat = extractor.getTrackFormat(i);
          String mime = trackFormat.getString(MediaFormat.KEY_MIME);
          if (mime.startsWith("audio/")) {
                audioTrack = i;
                hasAudio = true;
                break;
          }
      }
     上段代碼的意思,就是初始化了一個音視頻分離器,然後通過setDataSource給他設置了數據源,然後遍歷該數據源的所有信道,如果他KEY_MIME是“audio/”開頭的,說明他是音頻的信道,也就是我們所需要的音頻數據所在的信道。然後我們記錄下audioTrack。然後我們需要選中音頻信道
  if (hasAudio) {
         extractor.selectTrack(audioTrack);
         ...
  }
    接下來,我們就需要初始化一個混合器MediaMuxer了  
     MediaMuxer mediaMuxer = new MediaMuxer(audioSavePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
     MediaFormat trackFormat = extractor.getTrackFormat(audioTrack);
     int writeAudioIndex = mediaMuxer.addTrack(trackFormat);
     mediaMuxer.start();
     ByteBuffer byteBuffer = ByteBuffer.allocate(trackFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE));
     MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
    上面的代碼中,我們就初始化了一個MediaMuxer,並且設置了輸出文件的位置和輸出的格式,並且初始化了一個buffer緩衝區和一個用來保存視頻信息的BufferInfo,然後我們可以開始讀取和寫入數據了
 extractor.readSampleData(byteBuffer, 0);
 if (extractor.getSampleFlags() == MediaExtractor.SAMPLE_FLAG_SYNC) {
          extractor.advance();
 }
 while (true) {
          int readSampleSize = extractor.readSampleData(byteBuffer, 0);
          Log.e("hero","---讀取音頻數據,當前讀取到的大小-----:::"+readSampleSize);
          if (readSampleSize < 0) {
                    break;
           }

           bufferInfo.size = readSampleSize;
           bufferInfo.flags = extractor.getSampleFlags();
           bufferInfo.offset = 0;
           bufferInfo.presentationTimeUs = extractor.getSampleTime();
           Log.e("hero","----寫入音頻數據---當前的時間戳:::"+extractor.getSampleTime());

           mediaMuxer.writeSampleData(writeAudioIndex, byteBuffer, bufferInfo);
           extractor.advance();//移動到下一幀
  }
  mediaMuxer.release();
  extractor.release();

    上面的代碼,簡單的說就是通過readSampleData從分離器中讀取數據,如果沒有讀到說明分離完成了,就break掉,如果有數據,就設置當前幀的bufferInfo,比如偏移量,標誌,時間戳,當前幀大小等,然後把當前幀數據和幀信息寫入到混合器MediaMuxer中,然後開始讀取下一幀。
    這樣通過一個死循環,就可以讀取到媒體文件中我們指定信道的所有數據,並且通過混合器保存爲一個單獨的文件了,這樣就可以在android平臺將視頻中的音視頻進行分離而且不需要編解碼。

音頻文件轉PCM數據

    在上面我們已經從視頻中分離出了音頻文件,接下來,我們要將音頻文件進行解碼,還原成原始的PCM數據。將音頻進行解碼的方式有很多,但是大多都是c/c++的方式,比如ffmpeg等,不過,在android中,4.0之後的版本已經支持音視頻的硬編碼了,所以我們這裏用MediaCodec對音頻進行解碼,保存成PCM文件。MediaCodec是android平臺對音視頻進行編解碼的非常重要的一個類。我們的基本流程是從MediaExtacror裏面裏面讀取音頻信道的數據,給到音頻解碼器進行解碼,然後將解碼出來的數據寫到文件裏面。
    所謂的PCM文件,其實就是音頻在系統中保存的原始音頻數據,沒有經過編碼的,而我們常見的mp3,aac等是經過編碼的音頻數據。
    我們要解碼音頻,首先需要一個分離器,MediaExtactor,哈哈是不是很眼熟,是的,我們剛纔才使用了這個類,他不僅僅能讀取視頻文件,也可以讀取單個音頻文件,而且用法和上面分離音頻差別不大,那麼首先我們初始化一個,並且給他設置數據源。
     MediaExtractor extractor = new MediaExtractor();
     int audioTrack = -1;
     boolean hasAudio = false;
     try {
         extractor.setDataSource(audioPath);
         for (int i = 0; i < extractor.getTrackCount(); i++) {
             MediaFormat trackFormat = extractor.getTrackFormat(i);
             String mime = trackFormat.getString(MediaFormat.KEY_MIME);
             if (mime.startsWith("audio/")) {
                    audioTrack = i;
                    hasAudio = true;
                    break;
              }
          }
          if (hasAudio) {
              extractor.selectTrack(audioTrack);
               ...
          }
    同樣,因爲我們是要解碼音頻數據,所以就拿到音頻的信道。

    然後,接下來就是音頻解碼中非常重要的一些知識了,首先初始化音頻的解碼器
   MediaFormat trackFormat = extractor.getTrackFormat(audioTrack);
                //初始化音頻的解碼器
   MediaCodec audioCodec = MediaCodec.createDecoderByType(trackFormat.getString(MediaFormat.KEY_MIME));
   audioCodec.configure(trackFormat, null, null, 0);

   audioCodec.start();

   ByteBuffer[] inputBuffers = audioCodec.getInputBuffers();
   ByteBuffer[] outputBuffers = audioCodec.getOutputBuffers();
   MediaCodec.BufferInfo decodeBufferInfo = new MediaCodec.BufferInfo();
   MediaCodec.BufferInfo inputInfo = new MediaCodec.BufferInfo();
    上面的代碼,就是從分離器中拿到音頻信道的MediaFormat,然後通過MediaCodec.createDecoderbyType,初始化一個相應音頻格式的解碼器,MediaFormat.KEY_MIME的值,其實在MediaFormat中已經列舉出來了。
<Image_1>

     然後把要解碼音頻數據的mediaFormat通過audioCodec.configure方法進行設置,再通過start方法開啓解碼器,我們提前拿到了輸入的inputBuffer和輸出的outputBuffer。
     然後,我們整體的解碼過程就是,首先遍歷解碼器的所有inputBuffers,如果是可以使用的,就從分離器中讀取數據,並且給到inputBuffers。
    for (int i = 0; i < inputBuffers.length; i++) {
             //遍歷所以的編碼器 然後將數據傳入之後 再去輸出端取數據
             int inputIndex = audioCodec.dequeueInputBuffer(TIMEOUT_USEC);
             if (inputIndex >= 0) {
                      /**從分離器中拿到數據 寫入解碼器 */
                      ByteBuffer inputBuffer = inputBuffers[inputIndex];//拿到inputBuffer
                      inputBuffer.clear();//清空之前傳入inputBuffer內的數據
                      int sampleSize = extractor.readSampleData(inputBuffer, 0);//MediaExtractor讀取數據到inputBuffer中

                      if (sampleSize < 0) {
                              audioCodec.queueInputBuffer(inputIndex, 0, 0, 0L, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
                              inputDone = true;
                       } else {

                              inputInfo.offset = 0;
                              inputInfo.size = sampleSize;
                              inputInfo.flags = MediaCodec.BUFFER_FLAG_SYNC_FRAME;
                              inputInfo.presentationTimeUs = extractor.getSampleTime();

                               audioCodec.queueInputBuffer(inputIndex, inputInfo.offset, sampleSize, inputInfo.presentationTimeUs, 0);//通知MediaDecode解碼剛剛傳入的數據
                                extractor.advance();//MediaExtractor移動到下一取樣處
                        }
                 }
     }
     如果dequeueInputbuffer拿到的inputIndex = -1,說明這個輸入流不可用,如果可以用,我們就從輸入流的數組中取到相應位置的輸入流,然後清空之前遺留的數據,通過分離器的readSampleData讀取音頻數據,如果讀到的數據大小小於0,說明數據已經讀取完畢了,就將對應的輸入流插入一個流已經結束的標誌位
     MediaCodec.BUFFER_FLAG_END_OF_STREAM。大於0的話,就初始化輸入流的InputInfo,包括當前數據的時間戳,偏移量等等,然後通知解碼器對數據進行解碼audioCodec.queueInputBuffer。並且通過extactor.advance()將分離器移動到下一個取數據的地方。
      現在,我們已經往編碼器中寫入了很多數據,需要從解碼器的輸出流中讀取解碼後的數據了。
    while (!decodeOutputDone) {
          int outputIndex = audioCodec.dequeueOutputBuffer(decodeBufferInfo, TIMEOUT_USEC);
          if (outputIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
                    /**沒有可用的解碼器output*/
                    decodeOutputDone = true;
           } else if (outputIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
                    outputBuffers = audioCodec.getOutputBuffers();
           } else if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                    MediaFormat newFormat = audioCodec.getOutputFormat();
           } else if (outputIndex < 0) {
           } else {
                    ByteBuffer outputBuffer;
                    if (Build.VERSION.SDK_INT >= 21) {
                            outputBuffer = audioCodec.getOutputBuffer(outputIndex);
                    } else {
                            outputBuffer = outputBuffers[outputIndex];
                    }

                    chunkPCM = new byte[decodeBufferInfo.size];
                    outputBuffer.get(chunkPCM);
                    outputBuffer.clear();

                    fos.write(chunkPCM);//數據寫入文件中
                    fos.flush();
             }
             audioCodec.releaseOutputBuffer(outputIndex, false);
             if ((decodeBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                     /**
                      * 解碼結束,釋放分離器和解碼器
                      * */
                      extractor.release();

                      audioCodec.stop();
                      audioCodec.release();
                      codeOver = true;
                      decodeOutputDone = true;
               }
        }
        下面,我們來大致解釋一下上面的這些代碼,通過dequeueOutputBuffer遍歷,如果當前有可以使用的輸出流,就通過outputIndex拿到對應位置的OutputBuffer,並且從該輸出流緩衝區,讀取到byte數據,這個byte數據就是音頻解碼出來的最原始的音頻數據,其實就是一個byte數組。現實世界的聲音轉化成電腦或者手機系統裏面的數據的時候,其實大致過程就是通過取樣以及離散化,對脈衝信號進行編碼調製,最終轉化成PCM格式的數據,也就是一個byte的數組,這就是所有mp3或者aac等音頻格式解碼出來的在系統中的最原始數據,所謂的PCM格式的音頻數據其實可以簡單的理解爲一個byte數組,而爲什麼又會有mp3或者是aac的區別呢,因爲原始的PCM數據的數據量是非常大的,爲了便於保存和傳輸,就制定了許多不同的編碼格式,對一些雜音或者是不重要的數據進行分離,常見的就有mp3、AAC、wav等等,而這樣的編碼或多或少都是有損的編碼,而音質越好,他的數據量就越大,比如wav這種無損的編碼格式,同樣一首歌,wav格式的大小就遠遠大於了mp3或者是AAC。

       言歸正傳,在我們讀取到界面後的byte數據之後,我們就可以通過io,寫入到一個本地文件中,從輸出流中讀取到數據後,一定記得釋放到當前的輸出緩衝區 audioCodec.releaseOutputBuffer(outputIndex, false);

        當然如果我們已經讀取到了數據的末尾,就說明整個音頻已經解碼完畢了,我們就釋放掉分離器MediaExtactor和解碼器MediaCodec。
       
       通過上面兩個 兩個步驟,我們就完成了讀取一個音頻文件裏面的數據,並且將數據進行解碼保存成原始的PCM文件了,是不是非常簡單呢?當然我們可以將上面兩部分進行結合,就可以直接從一個視頻文件中讀取音頻數據並且進行解碼了。

       既然解碼如此簡單,那麼我們怎麼樣才能將解碼出來的PCM數據還原爲一個可以播放的音頻文件呢?下面這個我們就將介紹如何才能達成我們的目標。


PCM數據轉音頻文件

      在上面的兩部分內容中我們分別實現了從一個視頻文件中將音頻給分離出來,以及將一個音頻文件解碼成最原始的PCM數據。那麼現在我們來將PCM文件進行編碼成一個可以正常播放的音頻文件。基本思路是通過io流從PCM文件中讀取數據,然後將數據送到編碼器中,進行編碼,然後將編碼完成的數據,通過io流寫到一個文件中即可
      首先,我們需要初始化一個文件讀取流,從PCM文件中讀取數據
    FileInputStream fis = new FileInputStream(pcmPath);
      然後,初始化編碼器相關的東西
   int inputIndex;
   ByteBuffer inputBuffer;
   int outputIndex;
   ByteBuffer outputBuffer;
   byte[] chunkAudio;
   int outBitSize;
   int outPacketSize;
   //初始化編碼器
   MediaFormat encodeFormat = MediaFormat.createAudioFormat("audio/mp4a-latm", 44100, 2);//mime type 採樣率 聲道數
   encodeFormat.setInteger(MediaFormat.KEY_BIT_RATE, 96000);//比特率
   encodeFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
   encodeFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 500 * 1024);

   MediaCodec mediaEncode = MediaCodec.createEncoderByType("audio/mp4a-latm");
   mediaEncode.configure(encodeFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
   mediaEncode.start();

   ByteBuffer[] encodeInputBuffers = mediaEncode.getInputBuffers();
   ByteBuffer[] encodeOutputBuffers = mediaEncode.getOutputBuffers();
   MediaCodec.BufferInfo encodeBufferInfo = new MediaCodec.BufferInfo();
      上面的代碼可以看到,我們這裏的輸出音頻格式和採樣率,比特率等等都是寫死的,但是原始的音頻並不一定就是同樣的配置,所以可能會存在問題,比如你解碼的音頻是mp3格式,而我們這裏保存的是aac格式的文件,很明顯的一個差別就是aac文件的大小會比原始的mp3格式的音頻更小。
      接下來,我們初始化一個文件寫入流,可以讓我們把編碼出來的音頻數據寫入到一個aac文件中。
  FileOutputStream fos = new FileOutputStream(new File(audioPath));
  BufferedOutputStream bos = new BufferedOutputStream(fos, 500 * 1024);
     肯定,下面就是最核心的讀取數據—>給到編碼器—>編碼器的輸出流拿數據—>通過io寫入到文件中,這樣一個循環中
       boolean isReadEnd = false;
       while (!isReadEnd) {
            for (int i = 0; i < encodeInputBuffers.length - 1; i++) {
                  if (fis.read(buffer) != -1) {
                        allAudioBytes = Arrays.copyOf(buffer, buffer.length);
                   } else {
                        Log.e("hero", "---文件讀取完成---");
                        isReadEnd = true;
                        break;
                    }
                    Log.e("hero", "---io---讀取文件-寫入編碼器--" + allAudioBytes.length);
                    inputIndex = mediaEncode.dequeueInputBuffer(-1);
                    inputBuffer = encodeInputBuffers[inputIndex];
                    inputBuffer.clear();//同解碼器
                    inputBuffer.limit(allAudioBytes.length);
                    inputBuffer.put(allAudioBytes);//PCM數據填充給inputBuffer
                    mediaEncode.queueInputBuffer(inputIndex, 0, allAudioBytes.length, 0, 0);//通知編碼器 編碼
                }
                outputIndex = mediaEncode.dequeueOutputBuffer(encodeBufferInfo, 10000);//同解碼器
                while (outputIndex >= 0) {
                    //從編碼器中取出數據
                    outBitSize = encodeBufferInfo.size;
                    outPacketSize = outBitSize + 7;//7爲ADTS頭部的大小
                    outputBuffer = encodeOutputBuffers[outputIndex];//拿到輸出Buffer
                    outputBuffer.position(encodeBufferInfo.offset);
                    outputBuffer.limit(encodeBufferInfo.offset + outBitSize);
                    chunkAudio = new byte[outPacketSize];
                    AudioCodec.addADTStoPacket(chunkAudio, outPacketSize);//添加ADTS 代碼後面會貼上
                    outputBuffer.get(chunkAudio, 7, outBitSize);//將編碼得到的AAC數據 取出到byte[]中 偏移量offset=7 你懂得
                    outputBuffer.position(encodeBufferInfo.offset);
                    Log.e("hero", "--編碼成功-寫入文件----" + chunkAudio.length);
                    bos.write(chunkAudio, 0, chunkAudio.length);//BufferOutputStream 將文件保存到內存卡中 *.aac
                    bos.flush();

                    mediaEncode.releaseOutputBuffer(outputIndex, false);
                    outputIndex = mediaEncode.dequeueOutputBuffer(encodeBufferInfo, 10000);
                }
            }
            mediaEncode.stop();
            mediaEncode.release();
            fos.close();

      上面的代碼還是比較容易懂的,但是有個值得注意的點就是,首先我們每次從文件中讀取的byte的大小是有限制的,你不能一下子讀太多數據,如果一下子給太多數據編碼器會卡死,我們這裏限制的是8 * 1024 ,然後有一個就是addADTStopacket方法。
   /**
     * 寫入ADTS頭部數據
     * */
    public static void addADTStoPacket(byte[] packet, int packetLen) {
        int profile = 2; // AAC LC
        int freqIdx = 4; // 44.1KHz
        int chanCfg = 2; // CPE

        packet[0] = (byte) 0xFF;
        packet[1] = (byte) 0xF9;
        packet[2] = (byte) (((profile - 1) << 6) + (freqIdx << 2) + (chanCfg >> 2));
        packet[3] = (byte) (((chanCfg & 3) << 6) + (packetLen >> 11));
        packet[4] = (byte) ((packetLen & 0x7FF) >> 3);
        packet[5] = (byte) (((packetLen & 7) << 5) + 0x1F);
        packet[6] = (byte) 0xFC;
    }
      這是音頻編碼中非常重要的一個寫入ADTS頭部數據的操作,具體的原因,請查閱相關資料,我們這裏就不多說了,其實大概就是用幾個字符來表示我們音頻的格式,採樣率等等信息。
      通過上面幾個步驟,我們就完成了將一個很大的音頻的原始數據PCM進行編碼,然後寫入到一個音頻文件裏面的操作。
      可能有童鞋就會問了,那你這樣把音頻解碼—>PCM—>編碼成音頻,有什麼意義呢?其實這些非常有意義的事情,比如基於上面的這些功能,我們就可以完成很多的音頻操作的功能了,比如我們可以把兩個音頻分別進行解碼,然後依次讀取PCM文件中的數據進行編碼,從而實現兩個甚至多個音頻的連接,或者是音頻的變聲功能,將正常的聲音進行變調,從而實現變聲器的功能,再或者是將兩個音頻進行混音,也就是基於同一時間戳進行播放。
      看到音頻混音,可能有童鞋就不太明白要怎麼實現了,那麼下面的這部分內容,我們就來講解音頻混音的核心原理。

音頻的混音(單聲道和雙聲道的區別)

      這部分,我們來講解一些音頻混音的核心原理,所謂音頻混音就是將兩個甚至多個音頻基於同一個時間戳,同時進行播放,在android中可以通過實例化多個音頻播放器來實現,但是如何能編碼成一個文件呢?
      在上面講解PCM部分知識的時候,我們已經瞭解到了其實聲音在計算機系統中的存在形式其實就是一系列的byte數據,至於這些byte數據是如何通過離散數學轉換來的,我們這裏就不進行深入研究了。但是我們可以明白的是,我們都拿到最原始的byte數據了,那麼自然可以對他進行處理了,而所謂的音頻混音,其實就是一種處理。這樣的音頻處理算法其實在c/c++上面已經有非常多的很好的實現了,這不過我們這裏用java來實現一遍。
       其實音頻混音的核心原理就是將兩個音頻的原始byte數據進行疊加,非常簡單的 + 起來,比如某個位置的數據是1 而另一個音頻同樣位置是2 加起來就是3,這樣就完成了音頻的混音,當然這是最基礎也是最垃圾的混音算法,我們這裏會介紹其中的一種混音算法,基本上可以達到商業使用的。那就是歸一化混音算法。
       他的基本公式是
   C = A + B - A * B / (數據類型的最大值);
   byte數據就是
   C = A + B - A * B / 127;
   short數據就是
   C = A + B - A * B / 32767;
       爲什麼要進行 後面的減去操作呢?如果你使用byte進行數據操作的話,非常容易就出現數據大於最大值,而這樣的公式就是爲了避免這樣的情況出現,另外無法避免時,需要進行歸一化,也就是如果超出了範圍,就取最大值,如果小於了範圍就取最小值
      具體的實現,如下
    /**
     * 歸一化混音
     * */
    public static byte[] normalizationMix(byte[][] allAudioBytes){
        if (allAudioBytes == null || allAudioBytes.length == 0)
            return null;

        byte[] realMixAudio = allAudioBytes[0];

        //如果只有一個音頻的話,就返回這個音頻數據
        if(allAudioBytes.length == 1)
            return realMixAudio;

        //row 有幾個音頻要混音
        int row = realMixAudio.length /2;
        //
        short[][] sourecs = new short[allAudioBytes.length][row];
        for (int r = 0; r < 2; ++r) {
            for (int c = 0; c < row; ++c) {
                sourecs[r][c] = (short) ((allAudioBytes[r][c * 2] & 0xff) | (allAudioBytes[r][c * 2 + 1] & 0xff) << 8);
            }
        }

        //coloum第一個音頻長度 / 2
        short[] result = new short[row];
        //轉成short再計算的原因是,提供精確度,高端的混音軟件據說都是這樣做的,可以測試一下不轉short直接計算的混音結果
        for (int i = 0; i < row; i++) {
            int a = sourecs[0][i] ;
            int b = sourecs[1][i] ;
            if (a <0 && b<0){
                int i1 = a  + b  - a  * b / (-32768);
                if (i1 > 32767){
                    result[i] = 32767;
                }else if (i1 < - 32768){
                    result[i] = -32768;
                }else {
                    result[i] = (short) i1;
                }
            }else if (a > 0 && b> 0){
                int i1 = a + b - a  * b  / 32767;
                if (i1 > 32767){
                    result[i] = 32767;
                }else if (i1 < - 32768){
                    result[i] = -32768;
                }else {
                    result[i] = (short) i1;
                }
            }else {
                int i1 = a + b ;
                if (i1 > 32767){
                    result[i] = 32767;
                }else if (i1 < - 32768){
                    result[i] = -32768;
                }else {
                    result[i] = (short) i1;
                }
            }
        }
        return toByteArray(result);
    }
    public static byte[] toByteArray(short[] src) {
        int count = src.length;
        byte[] dest = new byte[count << 1];
        for (int i = 0; i < count; i++) {
            dest[i * 2 +1] = (byte) ((src[i] & 0xFF00) >> 8);
            dest[i * 2] = (byte) ((src[i] & 0x00FF));
        }
        return dest;
    }
    上面代碼,就是一個歸一化混音算法的java實現,而我們在過程中將原始是byte數據轉換成了short數據的原因就是爲了提高精度,從而讓混音效果更好。核心原理就是上面的公式。
     當然因爲音頻的原始數據其實是非常多的,爲了提升效率,最好使用jni實現混音相關算法,這樣就可以實現一個效果較好的混音算法了。項目裏面已添加相關實現,可以進行測試查閱。
     但是混音時,有一個問題需要注意一下,就是音頻是存在單聲道和雙聲道,立體聲的區別的。我們讀取音頻的信息的時候,可以看到他們是哪種聲道的,單聲道(mono),雙聲道(stereo),其實stereo應該叫立體聲,但是我查閱資料得到的信息是,大部分的android手機其實是不支持立體聲錄音的,android平臺的很多立體聲其實只是單純的雙聲道,因爲這涉及到非常底層的知識了,我也不太瞭解這一點,有知道的朋友,還望不惜賜教。這裏在進行混音的時候,不同的聲道會出現問題,因爲不同的聲道數據同樣的時間戳,播放的數據量是不同的,但是我們這裏混音是按照數據量來混的,所以一個單聲道和一個多聲道的音頻直接混音的話,就會出現混音失敗。那麼如何解決這個問題呢?
     其實我們可以通過將mono轉成stereo的方法來解決這個問題,居然的實現很簡單,如下代碼
  for (int i = 0; i < monoBytes.length; i += 2) {
            stereoBytes[i*2+0] = monoBytes[i];
            stereoBytes[i*2+1] = monoBytes[i+1];
            stereoBytes[i*2+2] = monoBytes[i];
            stereoBytes[i*2+3] = monoBytes[i+1];
  }
    

音頻的音量調節

     在上面,我們實現了音頻的混音,但是如果我們想要調節音頻原始音量的大小(不是通過手機音量鍵調節),我們應該怎麼做呢,比如如果要讓你實現給視頻增加bgm的時候,你bgm的音量不能太大以至於原視頻聲音聽不見了。其實所謂音量調節,還是很簡單,就是原始byte數據 乘上一定範圍內的數值,即可實現該功能。
     公式
    C = A * vol;//vol的取值範圍 通常是小於10,大於0的,如果是0的話,就沒有聲音了,如果太大了就會出現雜音
    vol的具體取值範圍需要自己多去測試,當然 這是最簡單的一種實現方式。

總結

    到這裏的話,本篇文章就基本上結束了,我們來回顧一下主要內容,首先,我們實現了從視頻裏面分離出音頻文件,然後將音頻文件解碼成最原始的PCM數據,再通過android平臺的硬編碼,將PCM數據的文件重新編碼成一個可以播放的音頻文件,接下來主要是講解了一些音頻混音的一些知識,實現了一個歸一化混音算法,然後說了一下不同聲道音頻的需要注意的一些問題,最後實現了一個改變原始音頻音量大小的功能。
    那麼,下一篇,按照計劃,我們將要通過OpenGL實現android平臺的視頻拼接功能,將不同的視頻完美的拼接在一起,當然並不是像音頻這樣的混音,而是在第一個視頻播放完畢之後,接着播放第二個視頻這種,而不是一個畫面中播放兩個視頻。
    因爲個人水平有限,難免有錯誤和不足之處,還望大家能包涵和提醒。謝謝啦!!!

其他
     相關代碼都已經更新到github上面了,項目的github地址,麻煩順手給個star,謝謝啦~




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