使用AudioTrack播放FFmpeg解碼的PCM音頻數(shù)據(jù)

開篇

在學(xué)習(xí)了《Android使用OpenGL渲染ffmpeg解碼的YUV數(shù)據(jù)》一文之后垦沉,我們的播放器計(jì)劃對于視頻的處理暫時(shí)先告一段落。后面的幾篇文章我們主要介紹ffmpeg解碼音頻并且搭配AudioTrack以及OpenSLES播放PCM原始音頻數(shù)據(jù)劣像。

音頻解碼

對于使用ffmpeg進(jìn)行音視頻的解碼過程乡话,我們來回憶一下這張圖:

ffmpeg解碼過程

其實(shí)音頻的解碼和視頻的解碼差不多,同樣要經(jīng)過解封裝耳奕,獲取流索引绑青、初始化解碼器上下文诬像、打開解碼器、av_read_frame讀取包數(shù)據(jù)闸婴、avcodec_send_packet發(fā)送到解碼器進(jìn)行解碼坏挠、avcodec_receive_frame接收解碼器數(shù)據(jù),這幾個(gè)過程邪乍。

不同的是降狠,視頻在avcodec_receive_frame接收到解碼數(shù)據(jù)是YUV,需要經(jīng)歷YUV轉(zhuǎn)換成RGB渲染出來庇楞。對于YUV不了解的同學(xué)榜配,可參考一下這篇文章《Android使用ffmpeg解碼視頻為YUV》

而對于音頻則在avcodec_receive_frame接收到的解碼數(shù)據(jù)是PCM原始聲音數(shù)據(jù)吕晌,而一般PCM數(shù)據(jù)都是無法直接播放的蛋褥,要按照播放的設(shè)備標(biāo)準(zhǔn)進(jìn)行重采樣之后生成新的PCM數(shù)據(jù)才能在特定的設(shè)備上播放。例如你解碼出來的PCM數(shù)據(jù)是32位的睛驳,但是你的設(shè)備只支持16位烙心,這就需要重采樣了。又比如說你的設(shè)備只支持播放44100的采樣率的乏沸,而你解碼出來的PCM卻不是44100的采樣率淫茵,那也是需要重采樣的,當(dāng)然實(shí)際情況不止這兩種蹬跃,這就是重采樣的意義所在匙瘪。

對于音頻的重采樣主要使用的是ffmpeg的swr_convert函數(shù),重采樣的主要代碼:

.....省略若干代碼

 //準(zhǔn)備音頻重采樣的參數(shù)

    int dataSize = av_samples_get_buffer_size(NULL, av_get_channel_layout_nb_channels(AV_CH_LAYOUT_STEREO) , cc_ctx->frame_size,AV_SAMPLE_FMT_S16, 0);

    uint8_t *resampleOutBuffer = (uint8_t *) malloc(dataSize);

    //音頻重采樣上下文初始化
    SwrContext *actx = swr_alloc();
    actx = swr_alloc_set_opts(actx,
                              AV_CH_LAYOUT_STEREO,
                              AV_SAMPLE_FMT_S16,44100,
                              cc_ctx->channels,
                              cc_ctx->sample_fmt,cc_ctx->sample_rate,
                              0,0 );
    re = swr_init(actx);
    if(re != 0)
    {
        LOGE("swr_init failed:%s",av_err2str(re));
        return re;
    }

.....省略若干代碼

//這里為什么要使用一個(gè)for循環(huán)呢炬转?
            // 因?yàn)閍vcodec_send_packet和avcodec_receive_frame并不是一對一的關(guān)系的
            //一個(gè)avcodec_send_packet可能會出發(fā)多個(gè)avcodec_receive_frame
            for (;;) {
                // 接受解碼的數(shù)據(jù)
                re = avcodec_receive_frame(cc_ctx, frame);
                if (re != 0) {
                    break;
                } else {

                    //音頻重采樣
                    int len = swr_convert(actx,&resampleOutBuffer,
                                          frame->nb_samples,
                                          (const uint8_t**)frame->data,
                                          frame->nb_samples);

                }
            }


使用AudioTrack播放PCM

對于AudioTrack如何使用還不了解的同學(xué)建議先自行谷歌學(xué)習(xí)一下辆苔,也就是幾個(gè)簡單的API,這里就不多做介紹了扼劈。

因?yàn)閒fmpeg的解碼是在JNI代碼中執(zhí)行的驻啤,所以我們直接在JNI通過反射調(diào)用Java的方法構(gòu)造出AudioTrack對象,然后傳遞數(shù)據(jù)給AudioTrack對象即可實(shí)現(xiàn)播放荐吵。

JNI構(gòu)建AudioTrack對象代碼:

 // JNI創(chuàng)建AudioTrack

    jclass jAudioTrackClass = env->FindClass("android/media/AudioTrack");
    jmethodID jAudioTrackCMid = env->GetMethodID(jAudioTrackClass,"<init>","(IIIIII)V"); //構(gòu)造

    //  public static final int STREAM_MUSIC = 3;
    int streamType = 3;
    int sampleRateInHz = 44100;
    // public static final int CHANNEL_OUT_STEREO = (CHANNEL_OUT_FRONT_LEFT | CHANNEL_OUT_FRONT_RIGHT);
    int channelConfig = (0x4 | 0x8);
    // public static final int ENCODING_PCM_16BIT = 2;
    int audioFormat = 2;
    // getMinBufferSize(int sampleRateInHz, int channelConfig, int audioFormat)
    jmethodID jGetMinBufferSizeMid = env->GetStaticMethodID(jAudioTrackClass, "getMinBufferSize", "(III)I");
    int bufferSizeInBytes = env->CallStaticIntMethod(jAudioTrackClass, jGetMinBufferSizeMid, sampleRateInHz, channelConfig, audioFormat);
    // public static final int MODE_STREAM = 1;
    int mode = 1;

    //創(chuàng)建了AudioTrack
    jobject jAudioTrack = env->NewObject(jAudioTrackClass,jAudioTrackCMid, streamType, sampleRateInHz, channelConfig, audioFormat, bufferSizeInBytes, mode);

    //play方法
    jmethodID jPlayMid = env->GetMethodID(jAudioTrackClass,"play","()V");
    env->CallVoidMethod(jAudioTrack,jPlayMid);

    // write method
    jmethodID jAudioTrackWriteMid = env->GetMethodID(jAudioTrackClass, "write", "([BII)I");

AudioTrack構(gòu)造好之后骑冗,我們在重采樣PCM數(shù)據(jù)之后直接再通過JNI傳遞PCM數(shù)據(jù)到AudioTrack就可以播放了。

下面貼一下完整的代碼:

extern "C"
JNIEXPORT jint JNICALL
Java_com_flyer_ffmpeg_FFmpegUtils_playAudio(JNIEnv *env, jclass clazz, jstring audio_path) {

    const char *path = env->GetStringUTFChars(audio_path, 0);

    AVFormatContext *fmt_ctx;
    // 初始化格式化上下文
    fmt_ctx = avformat_alloc_context();

    // 使用ffmpeg打開文件
    int re = avformat_open_input(&fmt_ctx, path, nullptr, nullptr);
    if (re != 0) {
        LOGE("打開文件失斚燃濉:%s", av_err2str(re));
        return re;
    }

    //探測流索引
    re = avformat_find_stream_info(fmt_ctx, nullptr);

    if (re < 0) {
        LOGE("索引探測失斣羯:%s", av_err2str(re));
        return re;
    }

    //尋找視頻流索引
    int audio_idx = av_find_best_stream(
            fmt_ctx, AVMEDIA_TYPE_AUDIO, -1, -1, nullptr, 0);

    if (audio_idx == -1) {
        LOGE("獲取音頻流索引失敗");
        return -1;
    }
    //解碼器參數(shù)
    AVCodecParameters *c_par;
    //解碼器上下文
    AVCodecContext *cc_ctx;
    //聲明一個(gè)解碼器
    const AVCodec *codec;

    c_par = fmt_ctx->streams[audio_idx]->codecpar;

    //通過id查找解碼器
    codec = avcodec_find_decoder(c_par->codec_id);

    if (!codec) {

        LOGE("查找解碼器失敗");
        return -2;
    }

    //用參數(shù)c_par實(shí)例化編解碼器上下文,薯蝎,并打開編解碼器
    cc_ctx = avcodec_alloc_context3(codec);

    // 關(guān)聯(lián)解碼器上下文
    re = avcodec_parameters_to_context(cc_ctx, c_par);

    if (re < 0) {
        LOGE("解碼器上下文關(guān)聯(lián)失敗:%s", av_err2str(re));
        return re;
    }

    //打開解碼器
    re = avcodec_open2(cc_ctx, codec, nullptr);

    if (re != 0) {
        LOGE("打開解碼器失敗:%s", av_err2str(re));
        return re;
    }

    //數(shù)據(jù)包
    AVPacket *pkt;
    //數(shù)據(jù)幀
    AVFrame *frame;

    //初始化
    pkt = av_packet_alloc();
    frame = av_frame_alloc();

    //音頻重采樣

    int dataSize = av_samples_get_buffer_size(NULL, av_get_channel_layout_nb_channels(AV_CH_LAYOUT_STEREO) , cc_ctx->frame_size,AV_SAMPLE_FMT_S16, 0);

    uint8_t *resampleOutBuffer = (uint8_t *) malloc(dataSize);

    //音頻重采樣上下文初始化
    SwrContext *actx = swr_alloc();
    actx = swr_alloc_set_opts(actx,
                              AV_CH_LAYOUT_STEREO,
                              AV_SAMPLE_FMT_S16,44100,
                              cc_ctx->channels,
                              cc_ctx->sample_fmt,cc_ctx->sample_rate,
                              0,0 );
    re = swr_init(actx);
    if(re != 0)
    {
        LOGE("swr_init failed:%s",av_err2str(re));
        return re;
    }

    // JNI創(chuàng)建AudioTrack

    jclass jAudioTrackClass = env->FindClass("android/media/AudioTrack");
    jmethodID jAudioTrackCMid = env->GetMethodID(jAudioTrackClass,"<init>","(IIIIII)V"); //構(gòu)造

    //  public static final int STREAM_MUSIC = 3;
    int streamType = 3;
    int sampleRateInHz = 44100;
    // public static final int CHANNEL_OUT_STEREO = (CHANNEL_OUT_FRONT_LEFT | CHANNEL_OUT_FRONT_RIGHT);
    int channelConfig = (0x4 | 0x8);
    // public static final int ENCODING_PCM_16BIT = 2;
    int audioFormat = 2;
    // getMinBufferSize(int sampleRateInHz, int channelConfig, int audioFormat)
    jmethodID jGetMinBufferSizeMid = env->GetStaticMethodID(jAudioTrackClass, "getMinBufferSize", "(III)I");
    int bufferSizeInBytes = env->CallStaticIntMethod(jAudioTrackClass, jGetMinBufferSizeMid, sampleRateInHz, channelConfig, audioFormat);
    // public static final int MODE_STREAM = 1;
    int mode = 1;

    //創(chuàng)建了AudioTrack
    jobject jAudioTrack = env->NewObject(jAudioTrackClass,jAudioTrackCMid, streamType, sampleRateInHz, channelConfig, audioFormat, bufferSizeInBytes, mode);

    //play方法
    jmethodID jPlayMid = env->GetMethodID(jAudioTrackClass,"play","()V");
    env->CallVoidMethod(jAudioTrack,jPlayMid);

    // write method
    jmethodID jAudioTrackWriteMid = env->GetMethodID(jAudioTrackClass, "write", "([BII)I");

    while (av_read_frame(fmt_ctx, pkt) >= 0) {//持續(xù)讀幀
        // 只解碼音頻流
        if (pkt->stream_index == audio_idx) {

            //發(fā)送數(shù)據(jù)包到解碼器
            avcodec_send_packet(cc_ctx, pkt);

            //清理
            av_packet_unref(pkt);

            //這里為什么要使用一個(gè)for循環(huán)呢遥倦?
            // 因?yàn)閍vcodec_send_packet和avcodec_receive_frame并不是一對一的關(guān)系的
            //一個(gè)avcodec_send_packet可能會出發(fā)多個(gè)avcodec_receive_frame
            for (;;) {
                // 接受解碼的數(shù)據(jù)
                re = avcodec_receive_frame(cc_ctx, frame);
                if (re != 0) {
                    break;
                } else {

                    //音頻重采樣
                    int len = swr_convert(actx,&resampleOutBuffer,
                                          frame->nb_samples,
                                          (const uint8_t**)frame->data,
                                          frame->nb_samples);

                    jbyteArray jPcmDataArray = env->NewByteArray(dataSize);
                    // native 創(chuàng)建 c 數(shù)組
                    jbyte *jPcmData = env->GetByteArrayElements(jPcmDataArray, NULL);

                    //內(nèi)存拷貝
                    memcpy(jPcmData, resampleOutBuffer, dataSize);

                    // 同步刷新到 jbyteArray ,并釋放 C/C++ 數(shù)組
                    env->ReleaseByteArrayElements(jPcmDataArray, jPcmData, 0);


                    LOGE("解碼成功%d  dataSize:%d ",len,dataSize);

                    // 寫入播放數(shù)據(jù)
                    env->CallIntMethod(jAudioTrack, jAudioTrackWriteMid, jPcmDataArray, 0, dataSize);

                    // 解除 jPcmDataArray 的持有占锯,讓 javaGC 回收
                    env->DeleteLocalRef(jPcmDataArray);

                }
            }

        }
    }

    //關(guān)閉環(huán)境
    avcodec_free_context(&cc_ctx);
    // 釋放資源
    av_frame_free(&frame);
    av_packet_free(&pkt);

    avformat_free_context(fmt_ctx);

    LOGE("音頻播放完畢");

    env->ReleaseStringUTFChars(audio_path, path);

    return 0;
}

結(jié)束

最后如果你對音視頻開發(fā)感興趣可掃碼關(guān)注袒哥,筆者在各個(gè)知識點(diǎn)學(xué)習(xí)完畢之后也會使用ffmepg從零開始編寫一個(gè)多媒體播放器缩筛,包括本地播放及網(wǎng)絡(luò)流播放等等。歡迎關(guān)注堡称,后續(xù)我們共同探討瞎抛,共同進(jìn)步。


image
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末却紧,一起剝皮案震驚了整個(gè)濱河市桐臊,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌晓殊,老刑警劉巖断凶,帶你破解...
    沈念sama閱讀 219,270評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異巫俺,居然都是意外死亡懒浮,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評論 3 395
  • 文/潘曉璐 我一進(jìn)店門识藤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人次伶,你說我怎么就攤上這事痴昧。” “怎么了冠王?”我有些...
    開封第一講書人閱讀 165,630評論 0 356
  • 文/不壞的土叔 我叫張陵赶撰,是天一觀的道長。 經(jīng)常有香客問我柱彻,道長豪娜,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,906評論 1 295
  • 正文 為了忘掉前任哟楷,我火速辦了婚禮瘤载,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘卖擅。我一直安慰自己鸣奔,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,928評論 6 392
  • 文/花漫 我一把揭開白布惩阶。 她就那樣靜靜地躺著挎狸,像睡著了一般。 火紅的嫁衣襯著肌膚如雪断楷。 梳的紋絲不亂的頭發(fā)上锨匆,一...
    開封第一講書人閱讀 51,718評論 1 305
  • 那天,我揣著相機(jī)與錄音冬筒,去河邊找鬼恐锣。 笑死茅主,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的侥蒙。 我是一名探鬼主播暗膜,決...
    沈念sama閱讀 40,442評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼鞭衩!你這毒婦竟也來了学搜?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,345評論 0 276
  • 序言:老撾萬榮一對情侶失蹤论衍,失蹤者是張志新(化名)和其女友劉穎瑞佩,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體坯台,經(jīng)...
    沈念sama閱讀 45,802評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡炬丸,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,984評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蜒蕾。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片稠炬。...
    茶點(diǎn)故事閱讀 40,117評論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡走越,死狀恐怖狈癞,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情驻龟,我是刑警寧澤撤摸,帶...
    沈念sama閱讀 35,810評論 5 346
  • 正文 年R本政府宣布毅桃,位于F島的核電站,受9級特大地震影響准夷,放射性物質(zhì)發(fā)生泄漏钥飞。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,462評論 3 331
  • 文/蒙蒙 一衫嵌、第九天 我趴在偏房一處隱蔽的房頂上張望读宙。 院中可真熱鬧,春花似錦渐扮、人聲如沸论悴。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽膀估。三九已至,卻和暖如春耻讽,著一層夾襖步出監(jiān)牢的瞬間察纯,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留饼记,地道東北人香伴。 一個(gè)月前我還...
    沈念sama閱讀 48,377評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像具则,于是被迫代替她去往敵國和親即纲。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,060評論 2 355

推薦閱讀更多精彩內(nèi)容