天天看點

ffmpeg開發播放器學習筆記 - 解碼音頻,使用AudioQueue 播放

作者:音視訊流媒體技術

該節是ffmpeg開發播放器學習筆記的第六節《ffmpeg解碼音頻,使用AudioQueue 播放》

ffmpeg音頻解碼後的資料是PCM(Pulse Code Modulation,脈沖編碼調制)音頻資料是未經壓縮的音頻采樣資料裸流,它是由模拟信号經過采樣、量化、編碼轉換成的标準數字音頻資料。對于我們最常說的“無損音頻”來說,一般都是指傳統CD格式中的16bit/44.1kHz采樣率的檔案格式,而知是以稱為無損壓縮,也是因為其包含了20Hz-22.05kHz這個完全覆寫人耳可聞範圍的頻響頻率而得名,當然現在的各種PCM格式編碼高碼率檔案已經層出不窮非常常見,但是就像上文中所說的,高碼率并不能有效地提升PCM編碼采樣率的頻響範圍,而隻能增加其采樣點來得到更加類似模拟錄音的平滑波形。

PCM音頻格式的播放在macOS/iOS平台可以使用AudioQueueu與AudioUint來播放,本節采用了AudioQueue。

ffmpeg開發播放器學習筆記 - 解碼音頻,使用AudioQueue 播放

✅ 第一節 - Hello FFmpeg

✅ 第二節 - 軟解視訊流,渲染 RGB24

✅ 第三節 - 認識YUV

✅ 第四節 - 硬解碼,OpenGL渲染YUV

✅ 第五節 - Metal 渲染YUV

第六節 - 解碼音頻,使用AudioQueue 播放

第七節 - 音視訊同步

第八節 - 完善播放控制

第九節 - 倍速播放

第十節 - 增加視訊過濾效果

第十一節 - 音頻變聲

目标

  • 了解PCM
  • 了解ffmpeg解碼同時播放音頻與視訊的流程
  • 了解AudioQueue播放流程
  • 完成ffmpeg解碼後的音頻與視訊同時播放

了解PCM

采樣率和采樣大小

聲音其實是一種能量波,是以也有頻率和振幅的特征,頻率對應于時間軸線,振幅對應于電平軸線。波是無限光滑的,弦線可以看成由無數點組成,由于存儲空間是相對有限的,數字編碼過程中,必須對弦線的點進行采樣。采樣的過程就是抽取某點的頻率值,很顯然,在一秒中内抽取的點越多,擷取得頻率資訊更豐富,為了複原波形,一次振動中,必須有2個點的采樣,人耳能夠感覺到的最高頻率為20kHz,是以要滿足人耳的聽覺要求,則需要至少每秒進行40k次采樣,用40kHz表達,這個40kHz就是采樣率。我們常見的CD,采樣率為44.1kHz。光有頻率資訊是不夠的,我們還必須獲得該頻率的能量值并量化,用于表示信号強度。量化電平數為2的整數次幂,我們常見的CD位16bit的采樣大小,即2的16次方。采樣大小相對采樣率更難了解,因為要顯得抽象點,舉個簡單例子:假設對一個波進行8次采樣,采樣點分别對應的能量值分别為A1-A8,但我們隻使用2bit的采樣大小,結果我們隻能保留A1-A8中4個點的值而舍棄另外4個。如果我們進行3bit的采樣大小,則剛好記錄下8個點的所有資訊。采樣率和采樣大小的值越大,記錄的波形更接近原始信号。

有損和無損

根據采樣率和采樣大小可以得知,相對自然界的信号,音頻編碼最多隻能做到無限接近,至少目前的技術隻能這樣了,相對自然界的信号,任何數字音頻編碼方案都是有損的,因為無法完全還原。在計算機應用中,能夠達到最高保真水準的就是PCM編碼,被廣泛用于素材儲存及音樂欣賞,CD、DVD以及我們常見的WAV檔案中均有應用。是以,PCM約定俗成了無損編碼,因為PCM代表了數字音頻中最佳的保真水準,并不意味着PCM就能夠確定信号絕對保真,PCM也隻能做到最大程度的無限接近。我們而習慣性的把MP3列入有損音頻編碼範疇,是相對PCM編碼的。強調編碼的相對性的有損和無損,是為了告訴大家,要做到真正的無損是困難的,就像用數字去表達圓周率,不管精度多高,也隻是無限接近,而不是真正等于圓周率的值。

使用音頻壓縮技術的原因

要算一個PCM音頻流的碼率是一件很輕松的事情,采樣率值×采樣大小值×聲道數 bps。一個采樣率為44.1KHz,采樣大小為16bit,雙聲道的PCM編碼的WAV檔案,它的資料速率則為 44.1K×16×2 =1411.2 Kbps。我們常說128K的MP3,對應的WAV的參數,就是這個1411.2 Kbps,這個參數也被稱為資料帶寬,它和ADSL中的帶寬是一個概念。将碼率除以8,就可以得到這個WAV的資料速率,即176.4KB/s。這表示存儲一秒鐘采樣率為44.1KHz,采樣大小為16bit,雙聲道的PCM編碼的音頻信号,需要176.4KB的空間,1分鐘則約為10.34M,這對大部分使用者是不可接受的,尤其是喜歡在電腦上聽音樂的朋友,要降低磁盤占用,隻有2種方法,降低采樣名額或者壓縮。降低名額是不可取的,是以專家們研發了各種壓縮方案。由于用途和針對的目标市場不一樣,各種音頻壓縮編碼所達到的音質和壓縮比都不一樣,在後面的文章中我們都會一一提到。有一點是可以肯定的,他們都壓縮過。

相關學習資料推薦,點選下方連結免費報名,先碼住不迷路~】

【免費分享】音視訊學習資料包、大廠面試題、技術視訊和學習路線圖,資料包括(C/C++,Linux,FFmpeg webRTC rtmp hls rtsp ffplay srs 等等)有需要的可以點選加群免費領取~

ffmpeg開發播放器學習筆記 - 解碼音頻,使用AudioQueue 播放

頻率與采樣率的關系

采樣率表示了每秒對原始信号采樣的次數,我們常見到的音頻檔案采樣率多為44.1KHz,這意味着什麼呢?假設我們有2段正弦波信号,分别為20Hz和20KHz,長度均為一秒鐘,以對應我們能聽到的最低頻和最高頻,分别對這兩段信号進行40KHz的采樣,我們可以得到一個什麼樣的結果呢?結果是:20Hz的信号每次振動被采樣了40K/20=2000次,而20K的信号每次振動隻有2次采樣。顯然,在相同的采樣率下,記錄低頻的資訊遠比高頻的詳細。這也是為什麼有些音響發燒友指責CD有數位聲不夠真實的原因,CD的44.1KHz采樣也無法保證高頻信号被較好記錄。要較好的記錄高頻信号,看來需要更高的采樣率,于是有些朋友在捕捉CD音軌的時候使用48KHz的采樣率,這是不可取的!這其實對音質沒有任何好處,對抓軌軟體來說,保持和CD提供的44.1KHz一樣的采樣率才是最佳音質的保證之一,而不是去提高它。較高的采樣率隻有相對模拟信号的時候才有用,如果被采樣的信号是數字的,請不要去嘗試提高采樣率。 流特征 随着網絡的發展,人們對線上收聽音樂提出了要求,是以也要求音頻檔案能夠一邊讀一邊播放,而不需要把這個檔案全部讀出後然後回放,這樣就可以做到不用下載下傳就可以實作收聽了;也可以做到一邊編碼一邊播放,正是這種特征,可以實作線上的直播,架設自己的數字廣播電台成為了現實。

編碼格式

PCM編碼

PCM 脈沖編碼調制是Pulse Code Modulation的縮寫。前面的文字我們提到了PCM大緻的工作流程,我們不需要關心PCM最終編碼采用的是什麼計算方式,我們隻需要知道PCM編碼的音頻流的優點和缺點就可以了。PCM編碼的最大的優點就是音質好,最大的缺點就是體積大。我們常見的Audio CD就采用了PCM編碼,一張CD光牒的容量隻能容納72分鐘的音樂資訊。

WAV格式

這是一種古老的音頻檔案格式,由微軟開發。WAV是一種檔案格式,符合RIFF (Resource Interchange File Format) 規範。所有的WAV都有一個檔案頭,這個檔案頭包含了音頻流的編碼參數。WAV對音頻流的編碼沒有硬性規定,除了PCM之外,還有幾乎所有支援ACM規範的編碼都可以為WAV的音頻流進行編碼。很多朋友沒有這個概念,我們拿AVI做個示範,因為AVI和WAV在檔案結構上是非常相似的,不過AVI多了一個視訊流而已。我們接觸到的AVI有很多種,是以我們經常需要安裝一些Decode才能觀看一些AVI,我們接觸到比較多的DivX就是一種視訊編碼,AVI可以采用DivX編碼來壓縮視訊流,當然也可以使用其他的編碼壓縮。同樣,WAV也可以使用多種音頻編碼來壓縮其音頻流,不過我們常見的都是音頻流被PCM編碼處理的WAV,但這不表示WAV隻能使用PCM編碼,MP3編碼同樣也可以運用在WAV中,和AVI一樣,隻要安裝好了相應的Decode,就可以欣賞這些WAV了。 在Windows平台下,基于PCM編碼的WAV是被支援得最好的音頻格式,所有音頻軟體都能完美支援,由于本身可以達到較高的音質的要求,是以,WAV也是音樂編輯創作的首選格式,适合儲存音樂素材。是以,基于PCM編碼的WAV被作為了一種中介的格式,常常使用在其他編碼的互相轉換之中,例如MP3轉換成WMA。

MP3編碼

MP3作為目前最為普及的音頻壓縮格式,為大家所大量接受,各種與MP3相關的軟體産品層出不窮,而且更多的硬體産品也開始支援MP3,我們能夠買到的VCD/DVD播放機都很多都能夠支援MP3,還有更多的便攜的MP3播放器等等,雖然幾大音樂商極其反感這種開放的格式,但也無法阻止這種音頻壓縮的格式的生存與流傳。MP3發展已經有10個年頭了,他是MPEG(MPEG:Moving Picture Experts Group) Audio Layer-3的簡稱,是MPEG1的衍生編碼方案,1993年由德國Fraunhofer IIS研究院和湯姆生公司合作發展成功。MP3可以做到12:1的驚人壓縮比并保持基本可聽的音質,在當年硬碟天價的日子裡,MP3迅速被使用者接受,随着網絡的普及,MP3被數以億計的使用者接受。MP3編碼技術的釋出之初其實是非常不完善的,由于缺乏對聲音和人耳聽覺的研究,早期的mp3編碼器幾乎全是以粗暴方式來編碼,音質破壞嚴重。随着新技術的不斷導入,mp3編碼技術一次一次的被改良,其中有2次重大技術上的改進。

除了以上編碼,還有其它的有興趣可以自己去google一下。

了解ffmpeg解碼同時播放音頻與視訊的流程

本節采用了解碼、播放音頻、渲染視訊各自己在單獨的線程中進行,共享資料緩沖區。解碼線程利用ffmpeg從資料流中讀取資料,并将音頻與視訊資料幀進行解碼轉碼後分别存儲到音頻與視訊緩沖區。

音頻播放與視訊渲染線程從音頻解碼線程緩沖區請求資料,流程圖如下:

  • 解碼線程維護兩個緩沖區: 音頻與視訊緩沖區
  • 音頻或視訊緩沖區緩沖資料未填滿時通知解碼線程開始進行解碼
  • 緩沖區資料填滿後解碼資料暫停,并通知音頻和視訊可以繼續播放(如果此時音頻或視訊緩沖區線程暫停)
  • 音頻播放線程從音頻緩沖區擷取資料,如果緩沖區資料不夠則暫停音頻緩沖區并通知解碼線程繼續解碼(如果解碼線程暫停)
  • 視訊渲染線程從視訊緩沖區擷取資料,如果緩沖區資料不夠則暫停視訊緩沖區并通知解碼線程繼續解碼(如果解碼線程暫停)

了解AudioQueue播放流程

AudioQueue是macOS/iOS平台的可直接播放PCM資料的音頻播放庫之後,提供了C語言接口,利用AudioQueu可以很友善的播放PCM資料。

1.初始化AudioQueue

初始化AudioQueue時需要提供一個目标PCM資料的描述,這個描述的結體構是這樣的:

AudioStreamBasicDescription asbd;
asbd.mSampleRate = audioInformation.rate;
asbd.mFormatID = kAudioFormatLinearPCM;
asbd.mChannelsPerFrame = 2;
asbd.mFramesPerPacket = 1;
asbd.mBitsPerChannel = 16;
asbd.mBytesPerFrame = 4;
asbd.mBytesPerPacket = 4;
/// kLinearPCMFormatFlagIsSignedInteger: 存儲的資料類型
/// kAudioFormatFlagIsPacked: 資料交叉排列
asbd.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
asbd.mReserved = NO;           

mSampleRate: 采樣率,描述了音頻采樣時的頻率(一秒時間對音頻波進行多少次資料采集)

mFormatID: 播放的資料格式

mChannelsPerFrame: 一幀音頻有多少個通道數(單聲道、雙聲道)

mFramesPerPacket: 一個資料包包含多少個音頻幀,PCM資料這個值是1

mBitsPerChannel: 一個資料通道占多少位資料

mBytesPerFrame: 一幀音頻資料占多少個位元組數

mBytesPerPacket: 一個資料久占多少個位元組數

mFormatFlags: 資料格式的具體描述

mReserved: 強制8位對齊,這裡必須設定成0

這個結構體具體描述的PCM資料的讀取方式,這很重要,資料讀取失敗播放出來的效果會很奇怪。

有了這個資料結構,就可以調用如下接口完成AudioQueue的初始化:

OSStatus status = AudioQueueNewOutput(&asbd, _AudioQueueOutputCallback, (__bridge void *)self, NULL, NULL, 0, &audioQueue);
NSAssert(status == errSecSuccess, @"Initialize audioQueue Failed");           

第一個參數是PCM資料描述的結構體指針

第二個參數是一個回調函數,在實際播放過程中需要重複利用AudioQueueBuffer以降低重複初始化AudioQueueBuffer的開銷

第三個參數是回調函數中的上下文參數,在C語言的函數指針中這樣的設計很常見,回調函數需要知道上下文并正确關聯對應的對象

最後一個參數則是初始化完成的AudioQueue對象 其中第二個參數的函數簽名是這樣的:

static void _AudioQueueOutputCallback(void *inUserData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer) {
    FFAudioQueuePlayer *player = (__bridge FFAudioQueuePlayer *)inUserData;
    [player reuseAudioQueueBuffer:inBuffer];
}           

2.初始化AudioQueueBuffer

AudioQueueBuffer是具體需要播放的音頻資料的載體,AudioQueueBuffer可以攜帶了多個音頻幀,完成AudioQueue的資料化之後就可以利用如下函數初始化足夠可重複利用的AudioQueueBuffer,這裡MAX_BUFFER_COUNT設定為3。

for(NSInteger i = 0; i < MAX_BUFFER_COUNT; i ++) {
    AudioQueueBufferRef audioQueueBuffer = NULL;
    status = AudioQueueAllocateBuffer(self->audioQueue, audioInformation.buffer_size, &audioQueueBuffer);
    NSAssert(status == errSecSuccess, @"Initialize AudioQueueBuffer Failed");
    CFArrayAppendValue(buffers, audioQueueBuffer);
}           

AudioQueueBuffer對象需要儲存并引用,防止被釋放。其次在後期的播放控制中也需要重新利用這些對象。

3.啟動并播放資料

AudioQueueStart(audioQueue, NULL);           

首先調用上面的代碼啟動播放器

AudioQueueBufferRef aqBuffer;
aqBuffer->mAudioDataByteSize = (int)length;
memcpy(aqBuffer->mAudioData, data, length);
AudioQueueEnqueueBuffer(self->audioQueue, aqBuffer, 0, NULL);
AudioQueueEnqueueBuffer(self->audioQueue, aqBuffer, 0, NULL);
           

設定好需要播放的資料大小與具體的資料之後,将AudioQueueBuffer對象放AudioQueue播放隊列即可播放出聲音。

完成ffmpeg解碼後的音頻與視訊同時播放

通過以上三步即可完成音頻資料的播放,但是在結合了ffmpeg之後除了音頻還需要播放視訊。

1.完善解碼

解碼在單獨的線程中進行,這裡為了友善說明流程邏輯使用的是僞代碼(完整的代碼請看示例工程),大緻邏輯是這樣的:

dispatch_async(decode_dispatch_queue, ^{
    while (true) {
        /// Video與Audio緩沖幀都超過最大數時暫停解碼線程,等待喚醒
        if((不需要解碼音頻 && 不需要解碼視訊) {
             sleep_for_wait();
        }
        AVFrame *frame = decode();
        if(is_audio(frame)) {
          audio_enqueue(frame);
          notify_audio_play_thread_play_if_wait();
        } else if(is_video(frame)) {
          video_enqueue(frame);
          notify_video_render_thread_render_if_wait();
        } else if(decode_complete) {
          /// 跳出while循環
          break;
        }
    }
    NSLog(@"Decode completed, read end of file.");
});           

2.完善視訊渲染

dispatch_async(video_render_dispatch_queue, ^{
    if(!has_enough_video_frame() && !isDecodeComplete) {
        notify_decode_thread_keep_decode();
        sleep_video_render_thread_for_wait();
    }
    AVFrame* frame = video_dequeue();
    if(obj) {
        video_render(frame);
        if(low_max_cache_video_frame()) {
          notify_decode_thread_keep_decode();
        }
    } else {
        if(isDecodeComplete) {
            stop_video_render();
        }
    }
});           

3.完善音頻播放

dispatch_async(audio_play_dispatch_queue, ^{
    if(!has_enough_audio_frame() && !isDecodeComplete) {
        notify_decode_thread_keep_decode();
        sleep_audio_play_thread_for_wait();
    }
    AVFrame* frame = audio_dequeue();
    if(obj) {
        audio_play(frame);
        if(low_max_cache_audio_frame()) {
          notify_decode_thread_keep_decode();
        }
    } else {
        if(isDecodeComplete) {
            stop_audio_play();
        }
    }
});           

解碼線程扮演了生産者的角色,在資料不夠的時候生産資料。音頻播放與視訊渲染扮演了消費者的角色,有足夠的資料後消費資料。資料緩沖區保持一個合理的動态平衡。

到此,播放器已經完成了視訊的渲染與音頻的播放。但是這還不夠,音頻與視訊在單獨的線程中不會是一種非常理想的同步播放的狀态,下一節将解決播放同步的問題。

總結

  • 了解PCM是什麼以及模拟音頻信号轉換成數字信号的過程與常用的編碼格式
  • 了解ffmpeg解碼同時播放音頻與視訊的流程,對播放器有了一個大緻的認識
  • 了解AudioQueue播放流程
  • 完成ffmpeg解碼後的音頻與視訊同時播放

原文 ffmpeg開發播放器學習筆記 - 解碼音頻,使用AudioQueue 播放

繼續閱讀