音頻編碼(一)——FFmpeg編碼

聲波

這里為啥講到了聲波,講到了我們的中學(xué)物理上的知識(shí),因?yàn)槲蚁氪蠹夷軓母纠斫夂竺嬉纛l編碼的各種參數(shù)以及原因。當(dāng)然這些知識(shí)網(wǎng)上都能搜到,我只是整合一下。

定義

聲音是由物體振動(dòng)產(chǎn)生的聲波,發(fā)聲體產(chǎn)生的振動(dòng)在空氣或其他物質(zhì)中的傳播叫做聲波。聲波借助各種介質(zhì)向四面八方傳播。這句話我們總結(jié)幾點(diǎn):

  • 聲音本質(zhì)是聲波
  • 聲波是由物體震動(dòng)產(chǎn)生
  • 聲波傳播需要介質(zhì)

關(guān)鍵名詞

振幅、周期、頻率這些我就不解釋了。我簡(jiǎn)單說下,振幅和音量相關(guān);頻率和單位時(shí)間的震動(dòng)次數(shù)有關(guān),進(jìn)一步說就是和音調(diào)有關(guān),有音樂理論基礎(chǔ)的朋友應(yīng)該知道,我們知道 do 、re 、mi..... ,我們看一下鋼琴的圖

piano.jpg

這里小字這一組的a1的的頻率就是440HZ,頻率越高,音調(diào)越高。每一種聲音都有各自的基本波形,稱為基波。不同聲音的基波中混入的諧波有多有少,導(dǎo)致音質(zhì)變化多端,也就是音色的不同。

我們現(xiàn)在總結(jié)下:聲音其實(shí)也是一種波,既然是波那就是有頻率和周期,當(dāng)然我們聽到的聲音可能是多個(gè)聲波干涉形成,可能是規(guī)則穩(wěn)定的波,
也可能是不規(guī)則的波。我們采集編碼,目的就是為了更加接近的記錄一定時(shí)間段內(nèi)聲波的形態(tài)。

抽樣

抽樣是把模擬信號(hào)以其信號(hào)帶寬2倍以上的頻率提取樣值,變?yōu)樵跁r(shí)間軸上離散的抽樣信號(hào)的過程。在音頻編碼上我們經(jīng)常會(huì)看到
44100的采樣頻率,人耳能識(shí)別的最高頻率大約是20kHZ,按我們剛在說的2倍以上的的頻率取樣值也比較的符合,我們常見的CD,采樣率為44.1kHz

低頻和高頻的采樣,比如:我們用44100HZ頻率對(duì)20000HZ的聲波采樣,那么每次震動(dòng),也就是每個(gè)聲波的周期只有大約2次采樣;當(dāng)我們?nèi)ゲ蓸?0HZ的聲波時(shí)候,每次震動(dòng)就采樣了大約2000次。所以我們知道對(duì)低頻的聲波能保證較好的記錄,對(duì)于較高頻率的聲波卻無法保證這也是為什么有些音響發(fā)燒友指責(zé)CD有數(shù)碼聲不夠真實(shí)的原因,CD的44.1KHz采樣也無法保證高頻信號(hào)被較好記錄。要較好的記錄高頻信號(hào),看來需要更高的采樣率。

有損和無損

所謂有損和無損都是相對(duì)而言,我們常說的無損是指采樣后的PCM音頻文件,包括封裝后的WAV都是無損的。同樣編碼后的MP3就是有損的。我們通常
參考的是PCM。那么PCM真正的是否有損呢?相對(duì)于自然的模擬信號(hào),當(dāng)然是有損的。聲音是連續(xù)的模擬信號(hào),要做到真正的無損是困難的,就像用數(shù)字去表達(dá)圓周率,不管精度多高,
也只是無限接近,而不是真正等于圓周率的值。

FFmpeg編碼PCM文件

需求:通過FFmpeg將PCM文件編碼成AAC文件,最終的文件我們可以進(jìn)行播放。
有朋友奇怪為什么要講將PCM編碼為AAC,而不是用Android設(shè)備采集再編碼輸出?我這樣介紹是有特殊考慮的,因?yàn)閺囊纛l采集到編碼輸出中間會(huì)有很多的坑,如果直接上手這一步,可能會(huì)出現(xiàn)各種問題。所以我們一步步來,先保證FFmpeg編碼PCM文件是沒問題的,我們?cè)龠M(jìn)行下一步,否則一次性調(diào)試太多東西,出問題你都不知道是哪里的問題。好了我們進(jìn)入主題。

測(cè)試文件:http://ovjkwgfx6.bkt.clouddn.com/pcm.zip 我們使用里面的"她的睫毛44100_16bit_雙聲道.pcm",當(dāng)然我們可以先打開這個(gè)文件看一下這個(gè)pcm,同時(shí)也可以停一下確保音頻是沒問題的,后面對(duì)編碼出來的aac文件進(jìn)行對(duì)比。

tdjm.png

大家也可以下載源碼運(yùn)行起來試一下:
注意:需要編碼的pcm文件需要放在Sd卡的FFmpegSample目錄下,代碼比較粗暴,沒有過多的交互,不會(huì)有什么編碼成功的彈窗等,請(qǐng)大家諒解。大家都是經(jīng)驗(yàn)豐富的攻城獅,所以大家點(diǎn)擊后,最好看log,里面會(huì)有編碼過程的信息。

shot1.png

入口在AudioRecordFFmpegActivity,代碼我就不全部貼了,只講核心邏輯:

第一步:初始化

各種初始化,打開封裝格式上下文等等,這些是FFmpge的基礎(chǔ),前面大家都用的比較數(shù)據(jù)了,就不說了

    av_register_all();

    avformat_alloc_output_context2(&pFormatCtx, NULL, NULL, out_file);
    fmt = pFormatCtx->oformat;

    //注意輸出路徑
    if (avio_open(&pFormatCtx->pb, out_file, AVIO_FLAG_READ_WRITE) < 0) {
        av_log(NULL, AV_LOG_ERROR, "%s", "輸出文件打開失??!\n");
        return -1;
    }

第二步:打開編碼器

首先需要找到編碼器:

    pCodec = avcodec_find_encoder(AV_CODEC_ID_AAC);
    if (!pCodec) {
        av_log(NULL, AV_LOG_ERROR, "%s", "沒有找到合適的編碼器!");
        return -1;
    }

第三步:新建一個(gè)流

傳遞的參數(shù)就是第一步初始化的封裝上下文和第二步找到的編碼器

    audio_st = avformat_new_stream(pFormatCtx, pCodec);
    if (audio_st == NULL) {
        av_log(NULL, AV_LOG_ERROR, "%s", "avformat_new_stream error");
        return -1;
    }

第三步:設(shè)置編碼器上下文的參數(shù)

這里的上下文就是從第三步中的流中得到

    pCodecCtx = audio_st->codec;
    pCodecCtx->codec_id = fmt->audio_codec;
    pCodecCtx->codec_type = AVMEDIA_TYPE_AUDIO;
    pCodecCtx->sample_fmt = outSampleFmt;
    pCodecCtx->sample_rate = sampleRate;
    pCodecCtx->channel_layout = AV_CH_LAYOUT_STEREO;
    pCodecCtx->channels = av_get_channel_layout_nb_channels(pCodecCtx->channel_layout);
    pCodecCtx->bit_rate = 64000;

這里的參數(shù)我們前面有定義:

    AVSampleFormat inSampleFmt = AV_SAMPLE_FMT_S16;
//    AVSampleFormat outSampleFmt = AV_SAMPLE_FMT_S16;
    AVSampleFormat outSampleFmt = AV_SAMPLE_FMT_FLTP;
    const int sampleRate = 44100;
    const int channels = 2;
    const int sampleByte = 2;

也就是格式是AV_SAMPLE_FMT_FLTP 、雙通道、位深是2個(gè)字節(jié)、頻率是44100。

第四步:打開編碼器

    if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) {
        av_log(NULL, AV_LOG_ERROR, "%s", "編碼器打開失??!\n");
        return -1;
    }

有朋友可能出現(xiàn)編碼器打開錯(cuò)誤,如果在第三步設(shè)置格式時(shí)使用的AV_SAMPLE_FMT_S16,那就會(huì)打開失敗,因?yàn)镕Fmpge默認(rèn)編碼器支持的輸入格式只能是AV_SAMPLE_FMT_FLTP。這里需要注意。

第五步:初始化重采樣上下文

    ///2 音頻重采樣 上下文初始化
    SwrContext *asc = NULL;
    asc = swr_alloc_set_opts(asc,
                             av_get_default_channel_layout(channels), outSampleFmt,
                             sampleRate,//輸出格式
                             av_get_default_channel_layout(channels), inSampleFmt, sampleRate, 0,
                             0);//輸入格式
    if (!asc) {
        av_log(NULL, AV_LOG_ERROR, "%s", "swr_alloc_set_opts failed!");
        return -1;
    }
    ret = swr_init(asc);
    if (ret < 0) {
        printAvError(ret);
        loge("swr_init error");
        return ret;
    }

前面我們提到過FFmpeg編碼器默認(rèn)支持輸入是輸入格式只能是AV_SAMPLE_FMT_FLTP,而我們PCM文件是 AV_SAMPLE_FMT_S16 ,所以需要進(jìn)行轉(zhuǎn)換后才能交給編碼器編碼。這里我們要用到SwrContext。

說到格式,就多說一點(diǎn)。正常我們從Android設(shè)備采集到的PCM數(shù)據(jù)是AV_SAMPLE_FMT_S16格式,也就是兩個(gè)聲道交替存儲(chǔ),每個(gè)樣點(diǎn)2個(gè)字節(jié)。而FFmpeg默認(rèn)的AAC編碼器不支持這種格式的編碼,只支持AV_SAMPLE_FMT_FLTP,這種格式是按平面存儲(chǔ),樣點(diǎn)是float類型,所謂平面也就是
每個(gè)聲道單獨(dú)存儲(chǔ),比如左聲道存儲(chǔ)到data[0]中,右聲道存儲(chǔ)到data[1]中。

第六步:初始化AVFrame

    frame = av_frame_alloc();
    frame->nb_samples = pCodecCtx->frame_size;
    frame->format = pCodecCtx->sample_fmt;
    av_log(NULL, AV_LOG_DEBUG, "sample_rate:%d,frame_size:%d, channels:%d", sampleRate,
           frame->nb_samples, frame->channels);
    //編碼每一幀的字節(jié)數(shù)
    size = av_samples_get_buffer_size(NULL, pCodecCtx->channels, pCodecCtx->frame_size,
                                      pCodecCtx->sample_fmt, 1);
    frame_buf = (uint8_t *) av_malloc(size);
    //一次讀取一幀音頻的字節(jié)數(shù)
    readSize = frame->nb_samples * channels * sampleByte;
    char *buf = new char[readSize];

    avcodec_fill_audio_frame(frame, pCodecCtx->channels, pCodecCtx->sample_fmt,
                             (const uint8_t *) frame_buf, size, 1);

這里主要看到av_samples_get_buffer_size方法,這個(gè)方法主要是計(jì)算編碼每一幀輸入給編碼器需要多少個(gè)字節(jié)。然后我們自己再分配空間,填充到初始化AVFrame中。這里我稍微講一點(diǎn)源碼,讓大家更清楚,這幾個(gè)方法的作用。

先看到av_samples_get_buffer_size

int av_samples_get_buffer_size(int *linesize, int nb_channels, int nb_samples,
                               enum AVSampleFormat sample_fmt, int align)
{
    int line_size;
    int sample_size = av_get_bytes_per_sample(sample_fmt);
    int planar      = av_sample_fmt_is_planar(sample_fmt);

    /* validate parameter ranges */
    if (!sample_size || nb_samples <= 0 || nb_channels <= 0)
        return AVERROR(EINVAL);

    /* auto-select alignment if not specified */
    if (!align) {
        if (nb_samples > INT_MAX - 31)
            return AVERROR(EINVAL);
        align = 1;
        nb_samples = FFALIGN(nb_samples, 32);
    }

    /* check for integer overflow */
    if (nb_channels > INT_MAX / align ||
        (int64_t)nb_channels * nb_samples > (INT_MAX - (align * nb_channels)) / sample_size)
        return AVERROR(EINVAL);

    line_size = planar ? FFALIGN(nb_samples * sample_size,               align) :
                         FFALIGN(nb_samples * sample_size * nb_channels, align);
    if (linesize)
        *linesize = line_size;

    return planar ? line_size * nb_channels : line_size;
}

這個(gè)方法根據(jù)channel,編碼器每一幀的采樣數(shù)、數(shù)據(jù)格式來計(jì)算每一幀所需要的存儲(chǔ)空間。首先如果是平面存儲(chǔ),那就是每個(gè)聲道單獨(dú)存放到data[0]、data[1]... 然后根據(jù)編碼器設(shè)置的sample_size和位深來計(jì)算每個(gè)通道需要的大小。最后算出整個(gè)一幀輸入需要的
大小。

接下來我們看看avcodec_fill_audio_frame

int avcodec_fill_audio_frame(AVFrame *frame, int nb_channels,
                             enum AVSampleFormat sample_fmt, const uint8_t *buf,
                             int buf_size, int align)
{
    int ch, planar, needed_size, ret = 0;

    needed_size = av_samples_get_buffer_size(NULL, nb_channels,
                                             frame->nb_samples, sample_fmt,
                                             align);
    if (buf_size < needed_size)
        return AVERROR(EINVAL);

    planar = av_sample_fmt_is_planar(sample_fmt);
    if (planar && nb_channels > AV_NUM_DATA_POINTERS) {
        if (!(frame->extended_data = av_mallocz_array(nb_channels,
                                                sizeof(*frame->extended_data))))
            return AVERROR(ENOMEM);
    } else {
        frame->extended_data = frame->data;
    }

    if ((ret = av_samples_fill_arrays(frame->extended_data, &frame->linesize[0],
                                      (uint8_t *)(intptr_t)buf, nb_channels, frame->nb_samples,
                                      sample_fmt, align)) < 0) {
        if (frame->extended_data != frame->data)
            av_freep(&frame->extended_data);
        return ret;
    }
    if (frame->extended_data != frame->data) {
        for (ch = 0; ch < AV_NUM_DATA_POINTERS; ch++)
            frame->data[ch] = frame->extended_data[ch];
    }

    return ret;
}

這里先做了一段校驗(yàn),然后主要看到av_samples_fill_arrays方法。

int av_samples_fill_arrays(uint8_t **audio_data, int *linesize,
                           const uint8_t *buf, int nb_channels, int nb_samples,
                           enum AVSampleFormat sample_fmt, int align)
{
    int ch, planar, buf_size, line_size;

    planar   = av_sample_fmt_is_planar(sample_fmt);
    buf_size = av_samples_get_buffer_size(&line_size, nb_channels, nb_samples,
                                          sample_fmt, align);
    if (buf_size < 0)
        return buf_size;

    audio_data[0] = (uint8_t *)buf;
    for (ch = 1; planar && ch < nb_channels; ch++)
        audio_data[ch] = audio_data[ch-1] + line_size;

    if (linesize)
        *linesize = line_size;

    return buf_size;
}

首先是獲取planar和buf_size,如果是planar格式那么就要走下面這段

    for (ch = 1; planar && ch < nb_channels; ch++)
        audio_data[ch] = audio_data[ch-1] + line_size;

設(shè)置每個(gè)通道數(shù)據(jù)的指針,所有的數(shù)據(jù)都是存在buf里,只是打包格式所有通道交替存,而planar格式要設(shè)置單獨(dú)設(shè)置指針來指向每個(gè)通道。

第七步 復(fù)制編碼器參數(shù),寫文件頭

第八步 編碼

    for (int i = 0;; i++) {
        //讀入PCM
        if (fread(buf, 1, readSize, in_file) < 0) {
            printf("文件讀取錯(cuò)誤!\n");
            return -1;
        } else if (feof(in_file)) {
            break;
        }
        frame->pts = apts;
        AVRational av;
        av.num = 1;
        av.den = sampleRate;
        apts += av_rescale_q(frame->nb_samples, av, pCodecCtx->time_base);
        int got_frame = 0;
        //重采樣源數(shù)據(jù)
        const uint8_t *indata[AV_NUM_DATA_POINTERS] = {0};
        indata[0] = (uint8_t *) buf;
        int len = swr_convert(asc, frame->data, frame->nb_samples, //輸出參數(shù),輸出存儲(chǔ)地址和樣本數(shù)量
                              indata, frame->nb_samples
        );
        //編碼
        ret = avcodec_send_frame(pCodecCtx, frame);
        if (ret < 0) {
            av_log(NULL, AV_LOG_ERROR, "%s", "avcodec_send_frame error\n");
        }

        ret = avcodec_receive_packet(pCodecCtx, &pkt);
        if (ret < 0) {
            av_log(NULL, AV_LOG_ERROR, "%s", "avcodec_receive_packet!error \n");
            printAvError(ret);
            continue;
        }
        pkt.stream_index = audio_st->index;
        av_log(NULL, AV_LOG_DEBUG, "第%d幀", i);
        pkt.pts = av_rescale_q(pkt.pts, pCodecCtx->time_base, audio_st->time_base);
        pkt.dts = av_rescale_q(pkt.dts, pCodecCtx->time_base, audio_st->time_base);
        pkt.duration = av_rescale_q(pkt.duration, pCodecCtx->time_base, audio_st->time_base);
        ret = av_write_frame(pFormatCtx, &pkt);
        if (ret < 0) {
            av_log(NULL, AV_LOG_ERROR, "av_write_frame error!");
        }
        av_packet_unref(&pkt);
    }
  • 首先是讀取原始數(shù)據(jù)fread(buf, 1, readSize, in_file)。這里的readSize就是我們前面計(jì)算的每一幀的大小。
  • 設(shè)置pts
  • 數(shù)據(jù)重采樣,使用swr_convert格式轉(zhuǎn)換
  • 編碼,輸出

第九步 寫文件尾,釋放資源

最后我們會(huì)在SD卡的的FFmpegSample目錄下找到tdjm.aac文件,我們發(fā)現(xiàn)編碼器是6.7M,編碼后326.4KB。當(dāng)然播放也是沒有問題的

tdjmaac.png

源碼地址: 音頻編碼(FFmpeg編碼一)
測(cè)試文件:http://ovjkwgfx6.bkt.clouddn.com/pcm.zip

注意:大家如果對(duì)代碼有不懂得地方,比如FFmpeg的so文件等,請(qǐng)看專題前面的文章。

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

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