FLV提取AAC音頻單獨播放並實現可視化的頻譜 視音頻編解碼學習工程:FLV封裝格式分析器

如上圖,要實現對FLV直播流中音頻的識別,並展現成一個音頻相關的動態頻譜。html

 

一. 首先了解下什麼是聲音?

能量波,有頻率有振幅,頻率高低就是音調,振幅大小就是音量;採樣率是對頻率採樣,採樣精度是對幅度採樣。html5

人耳能聽到的頻率範圍是200-20KHznode

音頻數字化就是將模擬的(連續的)聲音波形數字化(離散化),以便利用數字計算機進行處理的過程,主要參數包括採樣頻率(Sample Rate)和採樣數位/採樣精度(Quantizing,也稱量化級)兩個方面,這兩者決定了數字化音頻的質量。

git

二. 獲取音頻的可視化數據

音頻的可視化簡單來講能夠經過反覆收集當前音頻的時域數據, 並繪製爲一個示波器風格的輸出(頻譜)。github

時域time domain)是描述數學函數物理信號時間的關係。例如一個信號的時域波形能夠表達信號隨着時間的變化。web

頻域(frequency domain)是指在對函數信號進行分析時,分析其和頻率有關部分,而不是和時間有關的部分[1],和時域一詞相對。canvas

 

通常來講,可視化是經過獲取各個時間上的音頻數據(一般是振幅或頻率),以後運用圖像技術將其處理爲視覺輸出(例如一個圖像)來實現的。網頁音頻接口提供了一個不會改變輸入信號的音頻節點 AnalyserNode,經過它能夠獲取聲音數據並傳遞到像 <canvas> 等等同樣的可視化工具。segmentfault

 

 1. 什麼是AnalyserNode?以及如何建立AnalyserNode?

 AnalyserNode 賦予了節點能夠提供實時頻率及時間域分析的信息。它使一個 AudioNode經過音頻流不作修改的從輸入到輸出, 但容許你獲取生成的數據, 處理它並建立音頻可視化.數組

Without modifying the audio stream, the node allows to get the frequency and time-domain data associated to it, using a FFT. 

AnalyserNode是一個節點名稱,並非html5的API,它能夠經過 AudioContext 建立。瀏覽器

var audioCtx = new(window.AudioContext || window.webkitAudioContext)();
var analyser = audioCtx.createAnalyser();

 

AudioContext 即爲本文實現方案的一個重點API,它是html5處理音頻的API,MDN中解釋以下:

AudioContext接口表示由音頻模塊鏈接而成的音頻處理圖,每一個模塊對應一個AudioNodeAudioContext能夠控制它所包含的節點的建立,以及音頻處理、解碼操做的執行。作任何事情以前都要先建立AudioContext對象,由於一切都發生在這個環境之中。

 

總結一下實現方案就是,AudioContext建立一個AnalyserNode節點,經過該節點拿到頻譜數據(能夠理解爲必定範圍內的數字),進行圖形化顯示。

 

2. 那如何經過AnalyserNode節點獲取頻譜數據呢?

先上代碼:

this.analyser = this.audioCtx.createAnalyser()
this.analyser.fftSize = 1024
...
let array = new Uint8Array(this.analyser.frequencyBinCount)  // 建立frequencyBinCount長度的Uint8Array數組,用於存放音頻數據
this.analyser.getByteFrequencyData(array) // 將音頻數據填充到數組當中

 

這裏的array值即爲音頻的時域數據數組,數組中的每一個數據的最大值爲256。

爲何最大值爲256?
音頻的每一個數據佔用一個字節,當音頻無數據時,array中的值均爲0。每個字節有8位,最大值爲2的8次方,即256

 

再解釋幾個名詞:

  1. fftSize(Fast Fourier Transfor 快速傅里葉變換

AnalyserNode 接口的 fftSize 屬性的值是一個無符號長整型的值, 表示(信號)樣本的窗口大小。fftSize 屬性的值必須是從32到32768範圍內的2的非零冪; 其默認值爲2048。

  簡單理解即爲要獲取的音頻數據的長度。

  2. frequencyBinCount

  frequencyBinCount 的值固定爲 AnalyserNode 接口中fftSize值的一半. 該屬性一般用於可視化的數據值的數量。
  fftSize越大,可視化的數據值的數量越多,顯示的波形越細密。
 
   3. getByteFrequencyData

  getByteFrequencyData()方法將當前頻率數據複製到傳入的Uint8Array(無符號字節數組)中。

 

至此咱們已經獲取到能夠用於可視化的音頻數據數組!音頻數據已知,音頻數據的最大值已知,便可根據這些繪製出想要的可視化圖形。

細心的同窗可能發現,以上咱們並無接入任何音頻,那哪來的音頻數據?

對的,咱們還須要接入音頻才能拿到進行上面的這些操做。

 

三. 音頻的接入和播放

音頻源能夠提供一個片斷一個片斷的音頻採樣數據(以數組的方式),通常,一秒鐘的音頻數據能夠被切分紅幾萬個這樣的片斷。這些片斷能夠是通過一些數學運算獲得 (好比OscillatorNode),也能夠是音頻或視頻的文件讀出來的(好比AudioBufferSourceNodeMediaElementAudioSourceNode),又或者是音頻流(MediaStreamAudioSourceNode

音頻接入

方式一:createMediaElementSource

MediaElementAudioSourceNode 接口表明着某個由HTML5 <audio> 或 <video> 元素所組成的音頻源。

<audio id="audio" controls="" autoplay="" loop="" crossorigin="anonymous" src="./1.mp3"></audio>

<script>
    /*
        AudioContext.createMediaElementSource()
        建立一個MediaElementAudioSourceNode接口來關聯HTMLMediaElement. 這能夠用來播放和處理來自<video>或<audio> 元素的音頻.
    */
    var audio = document.getElementById('audio');

    var ctx = new AudioContext();
    var analyser = ctx.createAnalyser();
    var audioSrc = ctx.createMediaElementSource(audio);
    ...
  // 鏈接到音頻分析器,分析頻譜
    audioSrc.connect(analyser);  
    analyser.connect(ctx.destination);
</script>

AudioContextdestination 屬性返回一個 AudioDestinationNode 表示 context 中全部音頻(節點)的最終目標節點,通常是音頻渲染設備,好比揚聲器。

方式二:createBufferSource

AudioContext.createBufferSource() 建立一個 AudioBufferSourceNode 對象, 他能夠經過 AudioBuffer 對象來播放和處理包含在內的音頻數據。
AudioBuffer能夠用AudioContext 接口的 decodeAudioData() 方法異步解碼音頻文件中的 ArrayBuffer。ArrayBuffer數據能夠經過XMLHttpRequest和FileReader來獲取。
這是從音頻軌道建立用於web audio API音頻源的首選方法。
 
獲取到arrayBuffer後的播放步驟:
function decodeBuffer(arrayBuffer) {
    audioContext.decodeAudioData(arrayBuffer, function(buffer) {
        play(buffer);
    });
}
function play(buffer) {
    var audioBufferSourceNode = audioContext.createBufferSource();
    audioBufferSourceNode.connect(analyser);
    // 用於鏈接到終端設備進行播放聲音
    analyser.connect(audioContext.destination);
    audioBufferSourceNode.buffer = buffer;
    audioBufferSourceNode.start();
}

 

兩種獲取ArrayBuffer的方式一種是fileReader, 一種是XMLHttpRequest。

第一種方式:fileReader

<input id="fileChooser" type="file" />

<script>

window.onload = function() {

    var audioContext = new AudioContext();
    var analyser = audioContext.createAnalyser();
    analyser.fftSize = 256;

    var fileChooser = document.getElementById('fileChooser');
    fileChooser.onchange = function() {
        if (fileChooser.files[0]) {
            loadFile(fileChooser.files[0]);
        }
    }

        
    function loadFile(file) {
        var fileReader = new FileReader();
        fileReader.onload = function(e) {
        var arrayBuffer = e.target.result decodeBuffer(arrayBuffer); } fileReader.readAsArrayBuffer(file); }
} </script>

 

第二種:XHR(也是我獲取FLV音頻的方式)

getBuffer () {
    let _this = this
    // Fetch中的Response.body實現了getReader()方法用於漸增的讀取原始字節流
    // 處理器函數一塊一塊的接收響應體,而不是一次性的。當數據所有被讀完後會將done標記設置爲true。 在這種方式下,每次你只須要處理一個chunk,而不是一次性的處理整個響應體。

    let myRequest = new Request(this.config.url)
    fetch(myRequest, {
        method: 'GET'
    })
    .then(
        response => {
            _this._pump(response.body.getReader())
        },
        error => {
            console.error('audio stream fetch Error:', error)
        }
    )
    .catch((e) => {
        console.log('e:', e)
    })
  }

  _pump (reader) {
    var _this = this
    return reader.read()
            .then(
                ({ value, done }) => {
                    if (done) {
                        _this.debug('Stream reader done')
                    } else {
                        let arrayBuffer = value.buffer
                        ...

                        // 獲取下一個chunk
                        _this._pump(reader)
                    }
                })
            .catch((e) => {
                console.log('[flv audio]read stream:', e)
            })
  }

 至此,音頻源的接入和播放便可完成,但對於flv的音頻流,是不能直接用於 decodeAudioData 的,須要增長adts頭部信息方可decode。

 

四. Flv音頻的異步解碼

AAC ES流沒法直接播放,通常須要封裝爲ADTS格式才能再次使用,通常是在AAC ES流前添加7個字節的ADTS header。

ES--Elementary  Streams  (原始流)是直接從編碼器出來的數據流,能夠是編碼過的視頻數據流(H.264,MJPEG等),音頻數據流(AAC),或其餘編碼數據流的統稱。ES是隻包含一種內容的數據流,如只含視頻或只含音頻等。

什麼是ADTS header呢?能夠參考這篇

1. 那如何添加ADTS header呢?

 

在 視音頻編解碼學習工程:FLV封裝格式分析器 中介紹了FLV的封裝格式(如上圖),咱們能夠知道Flv body由若干個tag組成,每一個tag包含Tag Header和Tag Data部分,TagData部分又能夠分爲AudioTagHeader和AudioTagBody,以下:

(圖片來自:https://www.jianshu.com/p/d68d6efe8230)

 

AudioTagHeader包括音頻的配置信息有音頻編碼類型、採樣率、精度、類型,當SoundFormat爲10的時候,即當音頻是aac的時候,AudioTagHeader還包括一個字節的AACPacketType(值爲0或1),它表示後面的AudioTagBody是AudioSpecificConfig仍是AACframe data,以下圖:

(參考:https://www.adobe.com/content/dam/acom/en/devnet/flv/video_file_format_spec_v10.pdf

AAC sequence header包含了AudioSpecificConfig,有更詳細音頻的信息,但這種包只出現一次,並且是第一個Audio Tag,由於後面的音頻ES流須要該header的ADTS(Audio Data Transport Stream)頭。AAC raw則包含音頻ES流了,也就是audio payload。

註釋:<ui8> (8-Byte Unsigned Integer)

 有關AudioSpecificConfig的詳細信息能夠參考 ISO_IEC_14496 1.6.2.1

 ADTS的頭信息有7個字節,均可以從 AudioSpecificConfig 中獲取,上代碼:

  /**
    * 計算adts頭部, aac文件須要增長adts頭部才能被audioContext decode
    * @typedef {Object} AdtsHeadersInit
    * @property {number} audioObjectType
    * @property {number} samplingFrequencyIndex
    * @property {number} channelConfig
    * @property {number} adtsLen
    * @param {AdtsHeadersInit} init
    * 添加aac頭部參考:https://github.com/Xmader/flv2aac/blob/master/main.js
    */
  getAdtsHeaders (init) {
    const { audioObjectType, samplingFrequencyIndex, channelConfig, adtsLen } = init
    const headers = new Uint8Array(7)

    headers[0] = 0xff // syncword:0xfff                           高8bits
    headers[1] = 0xf0 // syncword:0xfff                           低4bits
    headers[1] |= (0 << 3) // MPEG Version:0 for MPEG-4,1 for MPEG-2   1bit
    headers[1] |= (0 << 1) // Layer:0                                  2bits
    headers[1] |= 1 // protection absent:1                      1bit

    headers[2] = (audioObjectType - 1) << 6 // profile:audio_object_type - 1                      2bits
    headers[2] |= (samplingFrequencyIndex & 0x0f) << 2 // sampling frequency index:sampling_frequency_index  4bits
    headers[2] |= (0 << 1) // private bit:0                                      1bit
    headers[2] |= (channelConfig & 0x04) >> 2 // channel configuration:channel_config               高1bit

    headers[3] = (channelConfig & 0x03) << 6 // channel configuration:channel_config     低2bits
    headers[3] |= (0 << 5) // original:0                               1bit
    headers[3] |= (0 << 4) // home:0                                   1bit
    headers[3] |= (0 << 3) // copyright id bit:0                       1bit
    headers[3] |= (0 << 2) // copyright id start:0                     1bit

    headers[3] |= (adtsLen & 0x1800) >> 11 // frame length:value    高2bits
    headers[4] = (adtsLen & 0x7f8) >> 3 // frame length:value    中間8bits
    headers[5] = (adtsLen & 0x7) << 5 // frame length:value    低3bits
    headers[5] |= 0x1f // buffer fullness:0x7ff 高5bits
    headers[6] = 0xfc

    return headers
  }

 

 其中 audioObjectType, samplingFrequencyIndex, channelConfig, adtsLen  便可從 AAC sequence header 中獲取,幸運的是,flv.js pr354 的做者已經把這部分信息解析出來了,省去了咱們不少麻煩。

在flv.js源碼的  demux/flv-demuxer.js  中,有_parseAudioData函數:

...
      if (aacData.packetType === 0) { // AAC sequence header (AudioSpecificConfig)
        if (meta.config) {
          Log.w(this.TAG, 'Found another AudioSpecificConfig!')
        }
        let misc = aacData.data
        meta.audioSampleRate = misc.samplingRate
        meta.channelCount = misc.channelCount
        meta.codec = misc.codec
        meta.originalCodec = misc.originalCodec
        meta.config = misc.config
        // added by qli5
        meta.configRaw = misc.configRaw
        // added by Xmader
        meta.audioObjectType = misc.audioObjectType
        meta.samplingFrequencyIndex = misc.samplingIndex
        meta.channelConfig = misc.channelCount
        // The decode result of Fan aac sample is 1024 PCM samples
        meta.refSampleDuration = 1024 / meta.audioSampleRate * meta.timescale
        Log.v(this.TAG, 'Parsed AudioSpecificConfig')

        if (this._isInitialMetadataDispatched()) {
          // Non-initial metadata, force dispatch (or flush) parsed frames to remuxer
          if (this._dispatch && (this._audioTrack.length || this._videoTrack.length)) {
            this._onDataAvailable(this._audioTrack, this._videoTrack)
          }
        } else {
          this._audioInitialMetadataDispatched = true
        }
        // then notify new metadata
        this._dispatch = false
        // metadata中的信息提供給外部封裝aac的adts頭部
        this._onTrackMetadata('audio', meta)

        let mi = this._mediaInfo
        mi.audioCodec = meta.originalCodec
        mi.audioSampleRate = meta.audioSampleRate
        mi.audioChannelCount = meta.channelCount
        if (mi.hasVideo) {
          if (mi.videoCodec != null) {
            mi.mimeType = 'video/x-flv; codecs="' + mi.videoCodec + ',' + mi.audioCodec + '"'
          }
        } else {
          mi.mimeType = 'video/x-flv; codecs="' + mi.audioCodec + '"'
        }
        if (mi.isComplete()) {
          this._onMediaInfo(mi)
        }
      }

...

 

 所以咱們能夠經過 _onTrackMetadata 得到metadata數據。接着咱們就能夠對AAC data添加ADTS頭部信息:

  /**
     * 獲取添加adts頭部信息的aac數據
     *
     * @param {*} metadata
     * @param {*} aac
     * @returns
     */
  getNewAac (aac) {
    const {
      audioObjectType,
      samplingFrequencyIndex,
      channelCount: channelConfig
    } = this.metadata

    let output = []
    let _this = this
    // aac音頻須要增長adts頭部後才能被解析播放
    aac.samples.forEach((sample) => {
      const headers = _this.getAdtsHeaders({
        audioObjectType,
        samplingFrequencyIndex,
        channelConfig,
        adtsLen: sample.length + 7
      })
      output.push(...headers, ...sample.unit)
    })

    return new Uint8Array(output)
  }

 

此時flv-demuxer.js具備兩大做用:

  1. 獲取ADTS頭部信息
  2. 獲取AAC ES流

最後咱們對ES流添加ADTS頭部,交給AudioContext.decodeAudioData解碼並播放。

(此處咱們只考慮利用flv-demuxer.js解析flv音頻的功能,處理視頻和MSE餵給video部分不考慮)

 

2. 對交給demuxer的chunk添加FLV header

每個被解析的flv音頻須要有一個header頭部,標誌flv的一些基本信息,以便flv-demuxer.js進行識別處理。
經過上文咱們能夠知道flv音頻最開始的9個字節即爲FlvHeader。咱們須要進行保存,而後對每一個Fetch Reader的音頻流都要先加flv header,再交給demuxer處理。
addFlvHeader (chunk) {
    let audioBuffer = null
    if (this.flvHeader == null) {
        // copy first 9 bytes (flv header)
        this.flvHeader = chunk.slice(0, 9)
        audioBuffer = chunk
    } else {
        audioBuffer = this.appendBuffer(this.flvHeader, chunk)
    }
    return audioBuffer
}

 

 五. FLV音頻的連續播放

 Fetch獲取音頻流是一段段的,每一段時間都很短,大概100ms左右,通過添加ADST頭部後,這些一段段的AAC音頻如何連續播放?如此高頻的解碼音頻是否有性能問題?

讓音頻連續的播放起來目前有兩種方式:

第一種堆積播放:

flv-demuxer.js默認的方式,會對以前的音頻進行堆積:

...
if
(aacData.packetType === 1) { // AAC raw frame data let dts = this._timestampBase + tagTimestamp let aacSample = { unit: aacData.data, length: aacData.data.byteLength, dts: dts, pts: dts } track.samples.push(aacSample) track.length += aacData.data.length }
...

 

每次從flv-demuxer.js獲取的AAC ES流都包含上一次解析的流內容,此時解碼後播放須要定位到上次播放的時間,以上次播放到的時間點爲起始點,播放當前的音頻流,播放時長爲本次流時長減去上次播放的流時長。

此種狀況下,利用AudioContext.decodeAudioData的音頻數據會愈來愈大,延時也就愈來愈高,消耗的性能也是愈來愈大。最終會致使瀏覽器的內存溢出,瀏覽器崩潰。

第二種分段播放:

此種狀況爲了不上種狀況的內存溢出,每次交給demuxer音頻數據時,先對 track.samples 進行清空:

// 清空audio以前的metadata數據
_this.flvDemuxerObj._audioMetadata = null
// 此爲清除以前的audio流,獲得fetch流對應的音頻;若不清除,parseChunk後獲得的是從開始累積的aac數據
_this.flvDemuxerObj._audioTrack = { type: 'audio', id: 2, sequenceNumber: 0, samples: [], length: 0 }

 

這樣每次從demuxer拿到的數據即爲Fetch Reader交給它的數據,沒有對歷史數據的累積。

每次播放時,只單獨播放每一個片斷的音頻數據。咱們會把處理好的音頻數據存放在音頻數組 audioStack 內,每次播放從數組內取出第一個  this.audioStack.shift() 

咱們會在上一段音頻播放結束後,進行出棧播放的操做:

audioBufferSourceNode.onended = function (e) {
    _this.loopPlayBuffers()
}

 

此時咱們忽略了從音頻出棧到audioContext播放此音樂的程序運行時間,其實是很是短暫的,咱們幾乎聽不出有停頓。但有一種狀況會產生延遲,在音頻出棧的時候,發現音頻棧爲空,此時多是由於網絡緣由fetch流產生較大的延遲,這個時候咱們必須等待有新的處理好的音頻入棧,才能接着播放,此時咱們就會感知到一個短暫的停頓。

計算延遲時間以下:

...

if (this.audioStack.length == 0) {
    console.warn('audioStack爲空,等待audio入棧(音頻解析速度慢或遇到問題)')
    this.delayStartTime = (new Date()).getTime()
    this.audioPlaying = false
    return
}

if (this.delayStartTime !== 0) {
    let nowTime = (new Date()).getTime()
    let gap = nowTime - this.delayStartTime
    this.delayStartTime = 0
    this.debugFunc('延遲時間:' + gap + ' ms')
}

...

 

六. 音頻可視化波形實現

 經過上文第二點可知咱們已經獲取到了音頻可視化的頻譜數據數組audioArray。

咱們只須要按照必定規則把數組數據繪製在canvas上便可。

這裏咱們實現一個圓形的音頻波形。

首先要理解圓形周圍的每一個柱形都是一個音譜數據,它的值value就是audioArray數組中的一個值,範圍爲[0-256]。

  canvas.height / 2 - round.r  爲波形的最大高度。那每一個音譜數據對應的柱形高度即爲:
let meterHeight = value * (wave.cheight / 2 - wave.cr) / 256 

咱們能夠自定義一個圓周要有多少個柱形組成,假設由 meterNum 表明柱形的個數,那咱們就要從audioArray中取樣,取出meterNum 個數據來:

// 計算採樣步長
var step = Math.round(array.length / meterNum)

而後咱們對audioArray頻譜數組每隔step個數據取一個樣本,進行柱形的繪製,並以圓心爲中心進行旋轉,旋轉的度數爲:

(360 / meterNum) * (wave.PI / 180)

 

for (let i = 0; i < meterNum; i++) {
      let value = array[i * step]
      // wave.cheight / 2 - wave.cr 爲波形的最大高度
      let meterHeight = value * (wave.cheight / 2 - wave.cr) / 256 || wave.minHeight
      // 根據圓心爲中心點旋轉
      wave.ctx.rotate((360 / meterNum) * (wave.PI / 180))
      wave.ctx.fillRect(-wave.meterWidth / 2, -wave.cr - meterHeight, wave.meterWidth, meterHeight)
    }
    wave.ctx.restore()
}

以上就是每一次繪製須要進行的操做,而後咱們利用  requestAnimationFrame  進行循環以上繪製。

 

以上部分的完整源代碼已經在github, 歡迎你們star試用,有任何問題也歡迎你們及時提出,一塊兒討論改進。

github地址:https://github.com/saysmy/flv-audio-visualization

 

 


2019-05-16補充更新:

第四大點:FLV音頻的異步解碼中 AACPacketType ,通常狀況下若是音頻格式標準,整個音頻流只有一個 AACPacketType 爲0的tag,但也有例外,有一些音頻流會出現屢次 AACPacketType值爲0的狀況, AACPacketType值爲0和1交替出現,不影響解碼:

 

 


 已知問題:

若是你的音視頻沒法播放,打開debug,發現有以下圖的warning提示:

則你的flv音視頻格式並不很規範,規範的flv音視頻解析的flvtrunk以下:

 

 它的前9個字節爲FLV Header,前三個字節是固定的70,76,86,表明文件標誌F、L、V。緊接着是4個字節的previousTagSize, 也是固定的0,0,0,0 ,由於它的前一個tag不存在,大小都爲0。再接着即是flv tag,第一個字節是tag type,通常是8(音頻),9(視頻),18(scriptData)。若是不是這樣的格式,就會解析失敗,出現上圖的warning提示。

 

附:

詳情見:https://cconcolato.github.io/media-mime-support/#audio/mp4;%20codecs=%22mp4a.40%22

 

參考文章:

https://lucius0.github.io/2017/12/27/archivers/media-study-03/

https://www.jianshu.com/p/d68d6efe8230

https://blog.csdn.net/tx3344/article/details/7414543

https://segmentfault.com/a/1190000017090438

https://github.com/Xmader/flv2aac

 

 


 

以上涉及源碼的github地址:https://github.com/saysmy/flv-audio-visualization

相關文章
相關標籤/搜索