天天看點

FFmpeg簡易播放器的實作3-音頻播放

作者: 葉餘 來源: https://www.cnblogs.com/leisure_chn/p/10068490.html

基于 FFmpeg 和 SDL 實作的簡易視訊播放器,主要分為讀取視訊檔案解碼和調用 SDL 播放兩大部分。本實驗僅研究音頻播放的實作方式,不考慮視訊。

FFmpeg 簡易播放器系列文章如下:

[1].

FFmpeg簡易播放器的實作1-最簡版 [2]. FFmpeg簡易播放器的實作2-視訊播放 [3]. FFmpeg簡易播放器的實作3-音頻播放 [4]. FFmpeg簡易播放器的實作4-音視訊播放 [5]. FFmpeg簡易播放器的實作5-音視訊同步

1. 視訊播放器基本原理

下圖引用自 “

雷霄骅,視音頻編解碼技術零基礎學習方法

”,因原圖太小,看不太清楚,故重新制作了一張圖檔。

FFmpeg簡易播放器的實作3-音頻播放

如下内容引用自 “

”:

解協定

将流媒體協定的資料,解析為标準的相應的封裝格式資料。視音頻在網絡上傳播的時候,常常采用各種流媒體協定,例如 HTTP,RTMP,或是 MMS 等等。這些協定在傳輸視音頻資料的同時,也會傳輸一些信令資料。這些信令資料包括對播放的控制(播放,暫停,停止),或者對網絡狀态的描述等。解協定的過程中會去除掉信令資料而隻保留視音頻資料。例如,采用 RTMP 協定傳輸的資料,經過解協定操作後,輸出 FLV 格式的資料。

解封裝

将輸入的封裝格式的資料,分離成為音頻流壓縮編碼資料和視訊流壓縮編碼資料。封裝格式種類很多,例如 MP4,MKV,RMVB,TS,FLV,AVI 等等,它的作用就是将已經壓縮編碼的視訊資料和音頻資料按照一定的格式放到一起。例如,FLV 格式的資料,經過解封裝操作後,輸出 H.264 編碼的視訊碼流和 AAC 編碼的音頻碼流。

解碼

将視訊/音頻壓縮編碼資料,解碼成為非壓縮的視訊/音頻原始資料。音頻的壓縮編碼标準包含 AAC,MP3,AC-3 等等,視訊的壓縮編碼标準則包含 H.264,MPEG2,VC-1 等等。解碼是整個系統中最重要也是最複雜的一個環節。通過解碼,壓縮編碼的視訊資料輸出成為非壓縮的顔色資料,例如 YUV420P,RGB 等等;壓縮編碼的音頻資料輸出成為非壓縮的音頻抽樣資料,例如 PCM 資料。

音視訊同步

根據解封裝子產品處理過程中擷取到的參數資訊,同步解碼出來的視訊和音頻資料,并将視訊音頻資料送至系統的顯示卡和聲霸卡播放出來。

2. 簡易播放器的實作-音頻播放

2.1 實驗平台

實驗平台:  openSUSE Leap 42.3  
FFmpeg版本:4.1  
SDL版本:   2.0.9      

FFmpeg 開發環境搭建可參考 “

ffmpeg開發環境建構

2.2 源碼流程分析

本實驗僅播放視訊檔案中的聲音,而不顯示圖像。源碼流程參考如下:

FFmpeg簡易播放器的實作3-音頻播放

2.3 源碼清單

使用如下指令下載下傳源碼:

svn checkout https://github.com/leichn/exercises/trunk/source/ffmpeg/player_audio      

2.4 關鍵過程

幾個關鍵函數的說明直接寫在代碼注釋裡:

2.4.1 開啟音頻處理子線程

// B2. 打開音頻裝置并建立音頻處理線程
// B2.1 打開音頻裝置,擷取SDL裝置支援的音頻參數actual_spec(期望的參數是wanted_spec,實際得到actual_spec)
// 1) SDL提供兩種使音頻裝置取得音頻資料方法:
//    a. push,SDL以特定的頻率調用回調函數,在回調函數中取得音頻資料
//    b. pull,使用者程式以特定的頻率調用SDL_QueueAudio(),向音頻裝置提供資料。此種情況wanted_spec.callback=NULL
// 2) 音頻裝置打開後播放靜音,不啟動回調,調用SDL_PauseAudio(0)後啟動回調,開始正常播放音頻
wanted_spec.freq = p_codec_ctx->sample_rate;    // 采樣率
wanted_spec.format = AUDIO_S16SYS;              // S表帶符号,16是采樣深度,SYS表采用系統位元組序
wanted_spec.channels = p_codec_ctx->channels;   // 聲道數
wanted_spec.silence = 0;                        // 靜音值
wanted_spec.samples = SDL_AUDIO_BUFFER_SIZE;    // SDL聲音緩沖區尺寸,機關是單聲道采樣點尺寸x通道數
wanted_spec.callback = sdl_audio_callback;      // 回調函數,若為NULL,則應使用SDL_QueueAudio()機制
wanted_spec.userdata = p_codec_ctx;             // 提供給回調函數的參數
if (SDL_OpenAudio(&wanted_spec, &actual_spec) < 0)
{
    printf("SDL_OpenAudio() failed: %s\n", SDL_GetError());
    goto exit4;
}
// B2.2 根據SDL音頻參數建構音頻重采樣參數
// wanted_spec是期望的參數,actual_spec是實際的參數,wanted_spec和auctual_spec都是SDL中的參數。
// 此處audio_param是FFmpeg中的參數,此參數應保證是SDL播放支援的參數,後面重采樣要用到此參數
// 音頻幀解碼後得到的frame中的音頻格式未必被SDL支援,比如frame可能是planar格式,但SDL2.0并不支援planar格式,
// 若将解碼後的frame直接送入SDL音頻緩沖區,聲音将無法正常播放。是以需要先将frame重采樣(轉換格式)為SDL支援的模式,
// 然後送再寫入SDL音頻緩沖區
s_audio_param_tgt.fmt = AV_SAMPLE_FMT_S16;
s_audio_param_tgt.freq = actual_spec.freq;
s_audio_param_tgt.channel_layout = av_get_default_channel_layout(actual_spec.channels);;
s_audio_param_tgt.channels =  actual_spec.channels;
s_audio_param_tgt.frame_size = av_samples_get_buffer_size(NULL, actual_spec.channels, 1, s_audio_param_tgt.fmt, 1);
s_audio_param_tgt.bytes_per_sec = av_samples_get_buffer_size(NULL, actual_spec.channels, actual_spec.freq, s_audio_param_tgt.fmt, 1);
if (s_audio_param_tgt.bytes_per_sec <= 0 || s_audio_param_tgt.frame_size <= 0)
{
    printf("av_samples_get_buffer_size failed\n");
    goto exit4;
}
s_audio_param_src = s_audio_param_tgt;      

2.4.2 啟動音頻回調機制

1
2
3
4
5
// 暫停/繼續音頻回調處理。參數1表暫停,0表繼續。
// 打開音頻裝置後預設未啟動回調處理,通過調用SDL_PauseAudio(0)來啟動回調處理。
// 這樣就可以在打開音頻裝置後先為回調函數安全初始化資料,一切就緒後再啟動音頻回調。
// 在暫停期間,會将靜音值往音頻裝置寫。
SDL_PauseAudio(0);      

2.4.3 音頻回調函數

使用者實作的函數,由 SDL 音頻處理子線程回調

// 音頻處理回調函數。讀隊列擷取音頻包,解碼,播放
// 此函數被SDL按需調用,此函數不在使用者主線程中,是以資料需要保護
// \param[in]  userdata使用者在注冊回調函數時指定的參數
// \param[out] stream 音頻資料緩沖區位址,将解碼後的音頻資料填入此緩沖區
// \param[out] len    音頻資料緩沖區大小,機關位元組
// 回調函數傳回後,stream指向的音頻緩沖區将變為無效
// 雙聲道采樣點的順序為LRLRLR
void audio_callback(void *userdata, uint8_t *stream, int len)
{
    ...
}      

2.4.4 音頻包隊列讀寫函數

使用者實作的函數,主線程向隊列尾部寫音頻包,SDL 音頻處理子線程(回調函數處理)從隊列頭部取出音頻包

// 寫隊列尾部
int packet_queue_push(packet_queue_t *q, AVPacket *pkt)
{
    ...
}
// 讀隊列頭部
int packet_queue_pop(packet_queue_t *q, AVPacket *pkt, int block)
{
    ...
}      

2.4.5 音頻解碼

音頻解碼功能封裝為一個函數,将一個音頻 packet 解碼後得到的聲音資料傳遞給輸出緩沖區。此處的輸出緩沖區 audio_buf 會由上一級調用函數 audio_callback() 在傳回時将緩沖區資料提供給音頻裝置。

int audio_decode_frame(AVCodecContext *p_codec_ctx, AVPacket *p_packet, uint8_t *audio_buf, int buf_size)
{
    AVFrame *p_frame = av_frame_alloc();
    
    int frm_size = 0;
    int res = 0;
    int ret = 0;
    int nb_samples = 0;             // 重采樣輸出樣本數
    uint8_t *p_cp_buf = NULL;
    int cp_len = 0;
    bool need_new = false;
    res = 0;
    while (1)
    {
        need_new = false;
        
        // 1 接收解碼器輸出的資料,每次接收一個frame
        ret = avcodec_receive_frame(p_codec_ctx, p_frame);
        if (ret != 0)
        {
            if (ret == AVERROR_EOF)
            {
                printf("audio avcodec_receive_frame(): the decoder has been fully flushed\n");
                res = 0;
                goto exit;
            }
            else if (ret == AVERROR(EAGAIN))
            {
                //printf("audio avcodec_receive_frame(): output is not available in this state - "
                //       "user must try to send new input\n");
                need_new = true;
            }
            else if (ret == AVERROR(EINVAL))
            {
                printf("audio avcodec_receive_frame(): codec not opened, or it is an encoder\n");
                res = -1;
                goto exit;
            }
            else
            {
                printf("audio avcodec_receive_frame(): legitimate decoding errors\n");
                res = -1;
                goto exit;
            }
        }
        else
        {
            // s_audio_param_tgt是SDL可接受的音頻幀數,是main()中取得的參數
            // 在main()函數中又有“s_audio_param_src = s_audio_param_tgt”
            // 此處表示:如果frame中的音頻參數 == s_audio_param_src == s_audio_param_tgt,那音頻重采樣的過程就免了(是以時s_audio_swr_ctx是NULL)
            //      否則使用frame(源)和s_audio_param_src(目标)中的音頻參數來設定s_audio_swr_ctx,并使用frame中的音頻參數來指派s_audio_param_src
            if (p_frame->format         != s_audio_param_src.fmt            ||
                p_frame->channel_layout != s_audio_param_src.channel_layout ||
                p_frame->sample_rate    != s_audio_param_src.freq)
            {
                swr_free(&s_audio_swr_ctx);
                // 使用frame(源)和is->audio_tgt(目标)中的音頻參數來設定is->swr_ctx
                s_audio_swr_ctx = swr_alloc_set_opts(NULL,
                                                     s_audio_param_tgt.channel_layout, 
                                                     s_audio_param_tgt.fmt, 
                                                     s_audio_param_tgt.freq,
                                                     p_frame->channel_layout,           
                                                     p_frame->format, 
                                                     p_frame->sample_rate,
                                                     0,
                                                     NULL);
                if (s_audio_swr_ctx == NULL || swr_init(s_audio_swr_ctx) < 0)
                {
                    printf("Cannot create sample rate converter for conversion of %d Hz %s %d channels to %d Hz %s %d channels!\n",
                            p_frame->sample_rate, av_get_sample_fmt_name(p_frame->format), p_frame->channels,
                            s_audio_param_tgt.freq, av_get_sample_fmt_name(s_audio_param_tgt.fmt), s_audio_param_tgt.channels);
                    swr_free(&s_audio_swr_ctx);
                    return -1;
                }
                
                // 使用frame中的參數更新s_audio_param_src,第一次更新後後面基本不用執行此if分支了,因為一個音頻流中各frame通用參數一樣
                s_audio_param_src.channel_layout = p_frame->channel_layout;
                s_audio_param_src.channels       = p_frame->channels;
                s_audio_param_src.freq           = p_frame->sample_rate;
                s_audio_param_src.fmt            = p_frame->format;
            }
            if (s_audio_swr_ctx != NULL)        // 重采樣
            {
                // 重采樣輸入參數1:輸入音頻樣本數是p_frame->nb_samples
                // 重采樣輸入參數2:輸入音頻緩沖區
                const uint8_t **in = (const uint8_t **)p_frame->extended_data;
                // 重采樣輸出參數1:輸出音頻緩沖區尺寸
                // 重采樣輸出參數2:輸出音頻緩沖區
                uint8_t **out = &s_resample_buf;
                // 重采樣輸出參數:輸出音頻樣本數(多加了256個樣本)
                int out_count = (int64_t)p_frame->nb_samples * s_audio_param_tgt.freq / p_frame->sample_rate + 256;
                // 重采樣輸出參數:輸出音頻緩沖區尺寸(以位元組為機關)
                int out_size  = av_samples_get_buffer_size(NULL, s_audio_param_tgt.channels, out_count, s_audio_param_tgt.fmt, 0);
                if (out_size < 0)
                {
                    printf("av_samples_get_buffer_size() failed\n");
                    return -1;
                }
                
                if (s_resample_buf == NULL)
                {
                    av_fast_malloc(&s_resample_buf, &s_resample_buf_len, out_size);
                }
                if (s_resample_buf == NULL)
                {
                    return AVERROR(ENOMEM);
                }
                // 音頻重采樣:傳回值是重采樣後得到的音頻資料中單個聲道的樣本數
                nb_samples = swr_convert(s_audio_swr_ctx, out, out_count, in, p_frame->nb_samples);
                if (nb_samples < 0) {
                    printf("swr_convert() failed\n");
                    return -1;
                }
                if (nb_samples == out_count)
                {
                    printf("audio buffer is probably too small\n");
                    if (swr_init(s_audio_swr_ctx) < 0)
                        swr_free(&s_audio_swr_ctx);
                }
        
                // 重采樣傳回的一幀音頻資料大小(以位元組為機關)
                p_cp_buf = s_resample_buf;
                cp_len = nb_samples * s_audio_param_tgt.channels * av_get_bytes_per_sample(s_audio_param_tgt.fmt);
            }
            else    // 不重采樣
            {
                // 根據相應音頻參數,獲得所需緩沖區大小
                frm_size = av_samples_get_buffer_size(
                        NULL, 
                        p_codec_ctx->channels,
                        p_frame->nb_samples,
                        p_codec_ctx->sample_fmt,
                        1);
                
                printf("frame size %d, buffer size %d\n", frm_size, buf_size);
                assert(frm_size <= buf_size);
                p_cp_buf = p_frame->data[0];
                cp_len = frm_size;
            }
            
            // 将音頻幀拷貝到函數輸出參數audio_buf
            memcpy(audio_buf, p_cp_buf, cp_len);
            res = cp_len;
            goto exit;
        }
        // 2 向解碼器喂資料,每次喂一個packet
        if (need_new)
        {
            ret = avcodec_send_packet(p_codec_ctx, p_packet);
            if (ret != 0)
            {
                printf("avcodec_send_packet() failed %d\n", ret);
                av_packet_unref(p_packet);
                res = -1;
                goto exit;
            }
        }
    }
exit:
    av_frame_unref(p_frame);
    return res;
}      

注意:

[1]. 一個音頻 packet 中含有多個完整的音頻幀,此函數每次隻傳回一個 frame,當 avcodec_receive_frame() 訓示需要新資料時才調用 avcodec_send_packet() 向編碼器發送一個 packet。

[2]. 音頻 frame 中的資料格式未必被 SDL 支援,對于不支援的音頻 frame 格式,需要進行重采樣,轉換為 SDL 支援的格式聲音才能正常播放

[3]. 解碼器内部會有緩沖機制,會緩存一定量的音頻幀,不沖洗(flush)解碼器的話,緩存幀是取不出來的,未沖洗(flush)解碼器情況下,avcodec_receive_frame() 傳回 AVERROR(EAGAIN),表示解碼器中改取的幀已取完了(當然緩存幀還是在的),需要用 avcodec_send_packet() 向解碼器提供新資料。

[4]. 檔案播放完畢時,應沖洗(flush)解碼器。沖洗(flush)解碼器的方法就是調用 avcodec_send_packet(..., NULL),然後按之前同樣的方式多次調用 avcodec_receive_frame() 将緩存幀取盡。緩存幀取完後,avcodec_receive_frame() 傳回 AVERROR_EOF。

3. 編譯與驗證

3.1 編譯

在源碼目錄運作:

./compiler.sh      

3.2 驗證

選用clock.avi測試檔案,測試檔案下載下傳(右鍵另存為):

clock.avi

檢視視訊檔案格式資訊:

ffprobe clock.avi      

列印視訊檔案資訊如下:

[avi @ 0x9286c0] non-interleaved AVI
Input #0, avi, from 'clock.avi':
  Duration: 00:00:12.00, start: 0.000000, bitrate: 42 kb/s
    Stream #0:0: Video: msrle ([1][0][0][0] / 0x0001), pal8, 320x320, 1 fps, 1 tbr, 1 tbn, 1 tbc
    Stream #0:1: Audio: truespeech ([34][0][0][0] / 0x0022), 8000 Hz, mono, s16, 8 kb/s      

運作測試指令:

./ffplayer clock.avi      

可以聽到每隔 1 秒播放一次“嘀”聲,播放 12 次後播放結束。播放過程隻有聲音,沒有圖像視窗。播放正常。

4. 參考資料

[1] 雷霄骅,

視音頻編解碼技術零基礎學習方法

[2] 雷霄骅,

最簡單的基于FFMPEG+SDL的視訊播放器ver2(采用SDL2.0)

[3] SDL WIKI,

https://wiki.libsdl.org/

[4] Martin Bohme,

An ffmpeg and SDL Tutorial, Tutorial 03: Playing Sound

5. 修改記錄

2018-12-04 V1.0 初稿

2019-01-06 V1.1 增加音頻重采樣,修複部分音頻格式無法正常播放的問題

「視訊雲技術」你最值得關注的音視訊技術公衆号,每周推送來自阿裡雲一線的實踐技術文章,在這裡與音視訊領域一流工程師交流切磋。
FFmpeg簡易播放器的實作3-音頻播放

繼續閱讀