Android音視頻【十一】視頻混音

人間觀察
其實人這一輩子 真的遇不到幾個真心對你好愛你的人 如果有幸能牽手 那就別并肩 好好的 別老是冷冰冰 說反話

簡介

短視頻的編輯功能有很多,比如:添加背景音樂,剪切,拼接視頻/音頻,特效,貼紙等等。
本文介紹為MP4視頻增加背景音樂(或者控制視頻原始的音量大?。?,其中涉及到音視頻的解碼,視頻和音頻的指定時長的提取,音頻視頻的分離,混合視頻音頻生成mp4,音頻混音,音量調(diào)節(jié)以及一些音頻處理細(xì)節(jié)和注意的地方等技術(shù)。采用的是Android的硬編解碼MediaCodec+ MediaExtractor+ MediaMuxer。

效果圖

混音-效果圖.png

處理方案/原理

混音原理圖.png

如圖所示如果我們想要把原始的mp4視頻增加背景音。我們需要如下幾個步驟。

  1. 選擇音頻軌道進(jìn)行解碼提取音頻,借助于MediaExtractorMediaCodec,然后將解碼后的pcm保存到臨時文件video.pcm
  2. 解碼背景音的mp3文件,借助于MediaCodec,然后將解碼后的pcm保存到臨時文件bgm.pcm
  3. 核心的一步pcm數(shù)據(jù)的處理,比如混音等,后文詳細(xì)介紹。然后混音后的pcm保存到臨時文件mix.pcm
  4. 混音后的pcm轉(zhuǎn)wav,主要是加入音頻的信息(采樣頻率,采樣位數(shù),聲道數(shù))在文件頭中加入42byte的wav的頭信息。
  5. 將wav編碼為aac音頻并寫入文件中。借助于MediaCodecMediaMuxer混合生成器
  6. 將原始視頻的中的視頻寫入到文件,借助于MediaMuxerMediaExtractor

第1,2步的解碼這里不多介紹了可以參考之前的文章。第4步pcm轉(zhuǎn)為wav也可以參考之前文章。第5,6步主要是MediaMuxer后文介紹下。主要是第2步重點介紹下。

混音

混音本質(zhì)就是對數(shù)據(jù)的處理。

PCM數(shù)據(jù)描述

我們先回顧下pcm的描述,也可以看下之前的音頻基礎(chǔ)那篇文章的介紹。我們這里說下采樣位數(shù)。

采樣位數(shù):表示每個采樣點用多少比特表示。常用的量化位數(shù)有8位、16位和32位。比如采樣位數(shù)為8bit時,每個采樣點可以表示256個不同的采樣值,當(dāng)采樣位數(shù)為16bit時,每個采樣點可以表示65536個不同的采樣值。采樣位數(shù)的大小影響聲音的質(zhì)量,采樣位數(shù)越多,量化后的波形越接近原始波形,聲音的質(zhì)量越高,需要的存儲空間就越大,反之同理。一般情況下,采樣位數(shù)是16bit,就像mp3的cd音質(zhì),你聽起來也沒那么失真。

采樣點數(shù)據(jù)有符號和無符號之分,比如:8bit的樣本數(shù)據(jù),有符號的是[-128 , 127],無符號的是[0,255 ]。16bit的樣本數(shù)據(jù),有符號的是 [-32768 , 32767],無符號的是[0, 65535]. 大多數(shù)pcm樣本數(shù)據(jù)使用整形表示,對于一些高精度的場景使用float 浮點型表示。對于我們平時看到的聽到的音視頻都是整形。

PCM的數(shù)據(jù)存儲方式

如果是單聲道音頻,采樣數(shù)據(jù)按照時間的先后順序依次存儲,如果是雙聲道音頻,則按照LRLRLRLR方式存儲,每個采樣點的存儲方式還與大小端有關(guān)。大端如下:

PCM數(shù)據(jù)存儲格式.png

另外MediaCodec解碼出來的pcm是按照LRLRLRLR方式存儲,但是FFmpeg解碼出來的pcm存儲格式很豐富(等后續(xù)學(xué)習(xí)FFmpeg時了解)。

此外即使同樣是signed 16 bits 有符號,也存在packed和planar的區(qū)別。對于雙聲道音頻來說,packed表示兩個聲道的數(shù)據(jù)交錯存儲,也就是LRLRLRLR的方式存儲;planar表示兩個聲道的數(shù)據(jù)分開存儲,也就是LLLLRRRR的方式存儲。

可以看到通過MediaCodec解碼出來的pcm是按照packed的方式存儲的,而FFmpeg可以是任何一種。所以為了統(tǒng)一化處理,需要進(jìn)行重采樣,比如你可以采用:每個采樣點按照2個字節(jié)的有符號的short類型,并且按照planar的方式存儲。

重采樣:
對pcm數(shù)據(jù)進(jìn)行重新采樣,可以改變它的聲道數(shù),采樣率和采樣格式。比如:原先的pcm音頻數(shù)據(jù)是2個聲道,44100采樣率,32 bit單精度型,可以重采樣為:2個聲道,44100采樣率,有符號short類型

本文的demo的視頻和音頻文件是按照采樣率是44100hz, 雙聲道 ,16位。使用的是MediaCodec解碼的音頻,所以為packed類型。

混音數(shù)據(jù)處理過程

  • 所以基于以上的pcm數(shù)據(jù)的存儲格式的原理,就可以進(jìn)行處理了。
    用2個buffer緩存區(qū)來保存從文件讀到的之前解碼后的視頻的pcm數(shù)據(jù)記為buffer1,另外一個為背景音樂的記為buffer2.混合后的記為buffer3.

也就是buffer1和buffer2 分別把連續(xù)的2個byte轉(zhuǎn)為short,前一個字節(jié)放在低八位后面的字節(jié)放在高八位。也就是:

short temp1 = (short) ((buffer1[i] & 0xff) | (buffer1[i + 1] & 0xff) << 8);
short temp2 = (short) ((buffer2[i] & 0xff) | (buffer2[i + 1] & 0xff) << 8);
  • 然后把2個short的值相加就得到了混音的值,在相加的過程中我們可以給temp1或者temp2分別乘以一個系數(shù)就可以達(dá)到控制各自音量(原始視頻和背景音樂的音量)的目的。想一想是吧!這樣我們就可以調(diào)節(jié)各自音量了。系數(shù)我們也要非常注意!,注意數(shù)據(jù)的精度丟失。所以為:
// videoVolume和 bgAudioVolume取值0-100
 val volume1: Float = videoVolume * 1.0f / 100
 val volume2: Float = bgAudioVolume * 1.0f / 100
 temp = (int) (temp1 * volume1 + temp2 * volume2);

temp就是混音后的值。但是還有一點是2個short類型的數(shù)字相加得到的結(jié)果有可能超過short的范圍[-32768 , 32767] ,所以需要控制一下最大最小值:

    if (temp > 32767) {
        temp = 32767;
    } else if (temp < -32768) {
        temp = -32768;
    }
  • 最后還原數(shù)據(jù),我們得到了混音后的數(shù)據(jù)為int型的,我們還得把它鋪開轉(zhuǎn)為byte類型
buffer3[i] = (byte) (temp & 0x00ff); // 低8位
buffer3[i + 1] = (byte) ((temp & 0xFF00) >> 8 ); // 高8位

最后混音的關(guān)鍵代碼(kotlin版)如下:

private fun mixPcm(
    pcm1: String,
    pcm2: String,
    mixPcm: String,
    videoVolume: Int,
    bgAudioVolume: Int
) {
    val volume1: Float = videoVolume * 1.0f / 100
    val volume2: Float = bgAudioVolume * 1.0f / 100
    val buffSize = 2048
    val buffer1 = ByteArray(buffSize)
    val buffer2 = ByteArray(buffSize)
    val buffer3 = ByteArray(buffSize)
    val fis1 = FileInputStream(pcm1)
    val fis2 = FileInputStream(pcm2)
    val fosMix = FileOutputStream(mixPcm)
    var isEnd1 = false
    var isEnd2 = false
    var temp1: Short
    var temp2: Short
    var temp: Int
    var ret1 = -1
    var ret2 = -1
    while (!isEnd1 || !isEnd2) {
        if (!isEnd1) {
            ret1 = fis1.read(buffer1)
            isEnd1 = ret1 == -1
        }
        if (!isEnd2) {
            ret2 = fis2.read(buffer2)
            isEnd2 = ret2 == -1
            for (i in buffer2.indices step 2) {
                // java 版本清楚些
#                 // temp1 = (short) ((buffer1[i] & 0xff) | (buffer1[i + 1] & 0xff) << 8);
                // temp2 = (short) ((buffer2[i] & 0xff) | (buffer2[i + 1] & 0xff) << 8);

//                    temp1 = PcmToWavUtil.to(buffer1[i], buffer1[i + 1])
//                    temp2 = PcmToWavUtil.to(buffer2[i], buffer2[i + 1])

                temp1 =
                    ((buffer1[i].toInt() and 0xff) or ((buffer1[i + 1].toInt() and 0xff) shl 8)).toShort()

                temp2 =
                    ((buffer2[i].toInt() and 0xff) or ((buffer2[i + 1].toInt() and 0xff) shl 8)).toShort()

                // 兩個short變量相加 會大于short
                temp = (temp1 * volume1 + temp2 * volume2).toInt()
                // short類型的取值范圍[-32768 ~ 32767]
                if (temp > 32767) {
                    temp = 32767
                } else if (temp < -32768) {
                    temp = -32768
                }

                // java 版本清楚些
                // buffer3[i] = (byte) (temp & 0x00ff);
                // buffer3[i + 1] = (byte) ((temp & 0xFF00) >> 8 );

                // 低八位 高八位 低八位 高八位 。。。
                buffer3[i] = (temp and 0x00ff).toByte()
                buffer3[i + 1] = (temp and 0xff00).shr(8).toByte()
            }
            fosMix.write(buffer3)
        }
    }
    fis1.close()
    fis2.close()
    fosMix.flush()
    fosMix.close()
    Log.d(TAG, "mixPcm:$mixPcm")
}

注意事項:
kotlin中,位運算只能是Int 和 Long類型,所以大部分我們都需要通過toInt()或者toLong()方法轉(zhuǎn)為Int或Long類型。比如

(buffer1[i].toInt() and 0xff)

語法糖有時候真的很坑,還不好理解。

關(guān)于性能

個人感覺MediaCodec 單純解碼音頻的解碼速度比較慢,視頻還不錯。FFmpeg對于從視頻中提取PCM較快(網(wǎng)上所說),等后續(xù)用FFmpeg的時候看下。

混合器MediaMuxer

在Android中我們可以使用MediaMuxer來封裝編碼后的視頻流和音頻流到mp4容器中。它僅支持一個視頻軌道(track)和一個音頻軌道(track)。
如果你有多個音頻軌道,那么需要把多個音頻軌道合為一個音頻軌道一起再使用MediaMuxer。也就是為什么要混音了。

  • 創(chuàng)建。指定輸出視頻的格式和文件目錄。
val mediaMuxer = MediaMuxer(output, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
  • 添加軌道信息。
    創(chuàng)建MediaMuxer對象之后,一個比較重要的操作就是addTrack(MediaFormat format),添加媒體通道,該函數(shù)需要傳入MediaFormat對象,通常從MediaExtractor或者M(jìn)ediaCodec中獲取,比如
val videoFormat = mediaExtractor.getTrackFormat(videoIndex)
val muxerVideoTrackIndex = mediaMuxer.addTrack(videoFormat)
        
val audioFormat = mediaExtractor.getTrackFormat(audioIndex)
val muxerAudioIndex = mediaMuxer.addTrack(audioFormat)
  • 啟動。 添加完所有track后調(diào)用start方法
 mediaMuxer.start()
  • 寫入音視頻數(shù)據(jù)
 public void writeSampleData(int trackIndex, @NonNull ByteBuffer byteBuf,
            @NonNull BufferInfo bufferInfo) ;

trackIndex 視頻軌道還是音頻軌道
byteBuf 為數(shù)據(jù)
bufferInfo 為數(shù)據(jù)的信息。

// bufferInfo屬性
info.size  數(shù)據(jù)的大小
info.flags  是否為關(guān)鍵幀
info.presentationTimeUs  PTS 時間戳,注意單位是 us
  • 停止寫入stop和釋放release
mediaMuxer.stop()
mediaMuxer.release()

源碼

https://github.com/ta893115871/VideoBGAdd

文章中的源碼采用kotlin,主要是自己學(xué)習(xí)下,因為工作中用不到。
用到的mp4,mp3為網(wǎng)絡(luò)上下載,只為學(xué)習(xí)使用,如有侵權(quán)將刪除。

結(jié)尾

本文介紹了視頻編輯功能的混音功能。涉及的知識點也不少,比如:pcm的數(shù)據(jù)格式,音頻的mp3硬解為pcm,音頻的硬編碼為aac,在視頻如何提取指定時長的視頻和音頻,如何把音頻和視頻混合為mp4,以及對數(shù)據(jù)的位運算等等。
其實主要是原理以及如何靈活的運用。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容