视频编解码学习二 pcm

  今天的主题是音频解码,主要实现将视频中的音频解码为音频采用格式pcm。
首先在文章开头给出两个问题

第一个问题:音频解码和视频解码目的是什么?为什么要进行音频压缩或者是视频压缩?
目的:压缩音频流、视频流、字幕流等等…(减小数据量)

第二个问题:音频采样数据作用?
保存音频中每一个采样点的值

我们来计算2分钟pcm音频的大小:
规定:采样率:44100HZ
在图像学中:每8位 = 1字节
编码(采样精度):16位 = 2字节
声道数量:2个
pcm格式体积 = 2 * 60 * 44100 * 2 * 2 = 21MB
mp3 = 2MB

从计算过程就可以看出,实际情况必须采用压缩来存储音频。

那么pcm有哪些格式,我们平常说的单声道,双声道又是什么回事?这里又分两种情况
第一种:单声道(左右声道)
第二种:双声道(排版顺序"左右","左右")
二者都是采样点顺序排版存储


image.png

那么音频解码的环节又是怎样的过程呢,参照官方给出的流程图

FFmpeg解码

从上图我们可以看出音频解码流程
第一步:注册所有的组件(编解码、滤镜特效处理库、封装格式处理库、工具库、音频采样数据格式转换库、视频像素数据格式转换等等...)
第二步:获取音频封装格式信息
第三步:查找音频流
第四步:查找音频解码器
第五步:打开音频解码器
第六步:读取音频压缩数据进行解码(循环解码)
第七步:关闭音频解码器释放内存

源码

#include <jni.h>
#include <string>
//导入android-log日志
#include <android/log.h>

//当前C++兼容C语言
extern "C"{
//avcodec:编解码(最重要的库)
#include "libavcodec/avcodec.h"
//avformat:封装格式处理
#include "libavformat/avformat.h"
//avutil:工具库(大部分库都需要这个库的支持)
#include "libavutil/imgutils.h"
//swscale:视频像素数据格式转换
#include "libswscale/swscale.h"
//导入音频采样数据格式转换库
#include "libswresample/swresample.h"

JNIEXPORT void JNICALL Java_com_samychen_ffmpeg_FFmpegTest_ffmpegTest
        (JNIEnv *, jobject);
JNIEXPORT void JNICALL Java_com_samychen_ffmpeg_FFmpegTest_ffmpegDecoder
        (JNIEnv *env, jobject jobj,jstring jInFilePath,jstring jOutFilePath);
JNIEXPORT void JNICALL Java_com_samychen_ffmpeg_FFmpegTest_ffmpegDecoderAudio
        (JNIEnv *env, jobject jobj,jstring jInFilePath,jstring jOutFilePath);
}

//1、NDK音视频编解码:FFmpeg-测试配置
JNIEXPORT void JNICALL Java_com_samychen_ffmpeg_FFmpegTest_ffmpegTest(
        JNIEnv *env, jobject jobj) {
    //(char *)表示C语言字符串
    const char *configuration = avcodec_configuration();
    __android_log_print(ANDROID_LOG_INFO,"main","%s",configuration);
}

//NDK音视频编解码:FFmpeg-音频解码-音频采样数据pcm格式
JNIEXPORT void JNICALL Java_com_samychen_ffmpeg_FFmpegTest_ffmpegDecoderAudio
        (JNIEnv *env, jobject jobj,jstring jInFilePath,jstring jOutFilePath) {

    //第一步:注册所有的组件
    // (编解码、滤镜特效处理库、封装格式处理库、工具库、音频采样数据格式转换库、视频像素数据格式转换等等...)
    av_register_all();
    avcodec_register_all();
    //第二步:获取音频封装格式信息
    AVFormatContext *avformat_context = avformat_alloc_context();
    //参数一:封装格式上下文->保存了音频信息
    //参数二:输入文件(你要对那一个文件进行解封装)
    //普及知识:env是JNI环境指针(作用:专门用于管理对象创建和销毁)
    const char *cInFilePath = env->GetStringUTFChars(jInFilePath, NULL);
    //参数三:封装格式类型(NULL:表示系统自动获取格式类型)
    //返回值:avformat_open_input_result = 0表示成功,否则失败
    int avformat_open_input_result = avformat_open_input(&avformat_context, cInFilePath, NULL,
                                                         NULL);
    if (avformat_open_input_result != 0) {
        char *error_info;
        av_strerror(avformat_open_input_result, error_info, 1024);
        __android_log_print(ANDROID_LOG_INFO, "main", "获取失败,错误信息:%s", error_info);
        return;
    }

    //第三步:查找音频流
    //返回值:>=0 if OK, AVERROR_xxx on error(>=0表示成功,否则失败)
    int avformat_find_stream_info_result = avformat_find_stream_info(avformat_context, NULL);
    if (avformat_find_stream_info_result < 0) {
        char *error_info;
        av_strerror(avformat_find_stream_info_result, error_info, 1024);
        __android_log_print(ANDROID_LOG_INFO, "main", "查找失败,错误信息:%s", error_info);
        return;
    }


    //第四步:查找音频解码器
    //第一点:查找音频流索引位置
    int av_stream_index_audio = -1;
    for (int i = 0; i < avformat_context->nb_streams; ++i) {
        if (avformat_context->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
            av_stream_index_audio = i;
            break;
        }
    }
    if (av_stream_index_audio == -1) {
        __android_log_print(ANDROID_LOG_INFO, "main", "没有找到音频流");
        return;
    }

    //第二点:查找音频解码器上下文(根据流索引位置->获取音频解码买上下文)
    //新的API
//    AVCodecParameters *avcodec_parameters = avformat_context->streams[av_stream_index_audio]->codecpar;
//    avcodec_parameters->codec_id;

    //老的API
    AVCodecContext *avcodec_context = avformat_context->streams[av_stream_index_audio]->codec;


    //第三点:根据音频解码器上下文->获取到->音频解码器
    AVCodec *avcodec = avcodec_find_decoder(avcodec_context->codec_id);
    if (avcodec == NULL) {
        __android_log_print(ANDROID_LOG_INFO, "main", "找不到这个音频解码器");
        return;
    }

    //第五步:打开音频解码器(调试运行->观察锁定C/C++基于NDK开发异常->便于调试运行)
    int avcodec_open2_result = avcodec_open2(avcodec_context, avcodec, NULL);
    if (avcodec_open2_result != 0) {
        char *error_info;
        av_strerror(avcodec_open2_result, error_info, 1024);
        __android_log_print(ANDROID_LOG_INFO, "main", "打开音频解码器失败,错误信息:%s", error_info);
        return;
    }


    //打印音频信息
    //输出视频信息
    //输出:文件格式
    __android_log_print(ANDROID_LOG_INFO, "main", "文件格式:%s", avformat_context->iformat->name);
    //输出:解码器名称
    __android_log_print(ANDROID_LOG_INFO, "main", "解码器名称:%s", avcodec->name);



    //第六步:读取音频压缩数据进行解码(循环解码)

    //读取一帧音频压缩数据(缓冲区)
    AVPacket *av_packet = (AVPacket *) av_malloc(sizeof(AVPacket));

    //接收一帧音频采样数据(缓冲区)
    AVFrame *av_frame_in = av_frame_alloc();

    //音频解码返回结果
    int avcodec_receive_frame_result;

    //音频采样数据上下文->SwrContext
    //第一点:创建上下文->开辟内存空间(声明)
    SwrContext *swr_context = swr_alloc();
    //第二点:给我们的音频采样数据上下文->绑定数据
    //参数一:音频采样数据上下文
    //参数二:输出声道布局类型(立体声、环绕、室内等等...)
    //立体声
    int out_ch_layout = AV_CH_LAYOUT_STEREO;
    //参数三:输出音频采样数据格式(说白了:采样精度)
    AVSampleFormat av_sample_format = AV_SAMPLE_FMT_S16;
    //参数四:输出音频采样数据->采样率
    int out_sample_rate = avcodec_context->sample_rate;
    //参数五:输入声道布局类型(立体声、环绕、室内等等...)->默认格式
    int in_ch_layout = av_get_default_channel_layout(avcodec_context->channels);
    //参数六:输入音频采样数据格式(说白了:采样精度)
    AVSampleFormat in_sample_fmt = avcodec_context->sample_fmt;
    //参数七:输入音频采样数据->采样率
    int in_sample_rate = avcodec_context->sample_rate;
    //参数八:Log日志偏移量
    //参数九:Log日志统计上下文
    swr_alloc_set_opts(swr_context,
                       out_ch_layout,
                       av_sample_format,
                       out_sample_rate,
                       in_ch_layout,
                       in_sample_fmt,
                       in_sample_rate,
                       0, NULL);

    //输出音频采样数据缓冲区(目标)->人的耳朵最大采样率->44100HZ
    int out_count = 16000;
    uint8_t *out_buffer = (uint8_t *) av_malloc(out_count);
    int pktsize, flush_complete = 0;
    //获取声道数量
    int out_nb_layout = av_get_channel_layout_nb_channels(out_ch_layout);

    //打开文件
    const char *coutputFilePath = env->GetStringUTFChars(jOutFilePath, NULL);
    FILE *out_file_pcm = fopen(coutputFilePath, "wb+");
    if (out_file_pcm == NULL) {
        __android_log_print(ANDROID_LOG_INFO, "main", "文件不存在");
        return;
    }

    int frame_index = 0;
//    while (av_read_frame(avformat_context, av_packet) == 0) {
//        ++frame_index;
//        if (av_packet->stream_index == av_stream_index_audio) {
//            out_buffer = av_packet->data;
//            pktsize = av_packet->size;
//            int frameFinished = 0;
//            int len = avcodec_decode_audio4(avcodec_context, av_frame_in, &frameFinished,
//                                            av_packet);
//            if (frameFinished) {
//                pktsize -= len;
//                out_buffer += len;
//                int data_size = av_samples_get_buffer_size(NULL,
//                                                           out_nb_layout,
//                                                           av_frame_in->nb_samples,
//                                                           av_sample_format,
//                                                           1);
//                /*****************************************************
//                以下代码使用swr_convert函数进行转换,但是转换后的文件连mp3到pcm文件都不能播放了,所以注释了
//                const uint8_t *in[] = {frame->data[0]};
//
//                int len=swr_convert(swrContext,out,sizeof(audio_buf)/codecContext->channels/av_get_bytes_per_sample(AV_SAMPLE_FMT_S16P),
//                    in,frame->linesize[0]/codecContext->channels/av_get_bytes_per_sample(codecContext->sample_fmt));
//
//                len=len*codecContext->channels*av_get_bytes_per_sample(AV_SAMPLE_FMT_S16P);
//
//                if (len < 0) {
//                    fprintf(stderr, "audio_resample() failed\n");
//                    break;
//                }
//                if (len == sizeof(audio_buf) / codecContext->channels / av_get_bytes_per_sample(AV_SAMPLE_FMT_S16P)) {
//                    fprintf(stderr, "warning: audio buffer is probably too small\n");
//                    swr_init(swrContext);
//                }
//                *****************************************************/
//                char *data = (char *) malloc(data_size);
//                short *sample_buffer = (short *) av_frame_in->data[0];
//                for (int i = 0; i < data_size / 2; i++) {
//                    data[i * 2] = (char) (sample_buffer[i / 2] & 0xFF);
//                    data[i * 2 + 1] = (char) ((sample_buffer[i / 2] >> 8) & 0xFF);
//
//                }
//                fwrite(data, data_size, 1, out_file_pcm);
//                fflush(out_file_pcm);
//            }
//        }
        //返回值:<0表示读取完毕,否则正在读取
        while (av_read_frame(avformat_context, av_packet) >= 0) {
            //判定当前帧是否是音频流->音频采样数据
            if (av_packet->stream_index == av_stream_index_audio) {
                //确定是我们的音频流->解码
                //解码一帧音频流数据
                //老的API
                //avcodec_decode_audio4();
                //新的API(发送->接收)
                //发送
                avcodec_send_packet(avcodec_context, av_packet);
                //接收
                avcodec_receive_frame_result = avcodec_receive_frame(avcodec_context, av_frame_in);

                if (avcodec_receive_frame_result == 0) {
                    //解码一帧音频压缩数据成功->得到了->一帧音频采样数据
                    //音频采样数据->转成pcm格式
                    //将输入->输出(pcm格式)
                    //参数一:音频采样数据上下文->SwrContext
                    //参数二:输出音频采样数据缓冲区(目标)
                    //参数三:输出缓冲区大小
                    //参数四:输入音频采样数据缓冲区
                    //参数五:输入缓冲区大小
                    swr_convert(swr_context,
                                &out_buffer,
                                out_count,
                                (const uint8_t **) av_frame_in->data,
                                av_frame_in->nb_samples);
                    //获取缓冲区实际数据大小
                    //参数一:行大小
                    //参数二:声道数量
                    //参数三:输出大小
                    //参数四:输出音频采样数据格式
                    //参数五:字节对齐类型
                    int out_buffer_size = av_samples_get_buffer_size(NULL,
                                                                     out_nb_layout,
                                                                     av_frame_in->nb_samples,
                                                                     av_sample_format,
                                                                     1);
                    //写入文件
                    fwrite(av_frame_in->data[0], 1, out_buffer_size, out_file_pcm);
                    fflush(out_file_pcm);
                    frame_index++;
                    __android_log_print(ANDROID_LOG_INFO, "main", "当前是第%d帧", frame_index);
                    __android_log_print(ANDROID_LOG_INFO, "main", "当前是第%d帧", out_buffer);
                }
            }
        }
        //第七步:关闭音频解码器释放内存
        av_packet_free(&av_packet);
        //关闭流
        fclose(out_file_pcm);

        swr_free(&swr_context);

        av_free(out_buffer);

        avcodec_close(avcodec_context);
        avformat_free_context(avformat_context);

    }

是不是和视频解码大体流程完全一样,唯一的区别就是视频像素数据格式的转换和音频采样数据的重新采样了。
上述代码实现完成之后可以在pc端下载Adobe audition cs6来直接播放

下载

https://github.com/samychen/SDL_FFmpeg_Tutorial

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容

  • 当我老了的时候 悠然南山不过痴想 我不想老去 不想最后沉睡于盒中 一生最后只是一张照片 虽生死仍然 当我老了的时候...
    爱腾讯阅读 239评论 1 1
  • 那天,雨下得很大,我撑着伞,你在我旁边,在伞下,没淋湿。 睡眼还是惺忪 梦未醒 雨还在下 雨滴敲在窗上 滴滴答答 ...
    独立日的折耳猫阅读 516评论 5 10
  • 感觉有好多事情要做,可是。好累。
    昵称已呗使用阅读 146评论 2 0