美文网首页
4. 【音频编解码实战】

4. 【音频编解码实战】

作者: 东也_ | 来源:发表于2022-04-25 09:31 被阅读0次

    音频重采样

    就是将音频三元组(采样率 采样大小 通道数)的值转成另外一组值

    1. 应用场景:

    1、从设备采集的音频数据与编码器要求的不一致;
    2、扬声器要求的音频数据与要播放的音频数据不一致;
    3、方便运算:例如回音消除 将多声道变为单声道;

    2. 如何判断是否需要重采样

    • 了解音频设备的参数
    • 查看ffmpeg源码

    3. 重采样的步骤

    api:需要使用libswresample库

    1. 创建重采样上下文
     - swr_alloc_set_opts 通过设置采样参数获取上下文      
    
    2. 设置参数
    - 参数大体分为输出的采样率、采样大小、声道和输入的采样率、采样大小、声道;
    - out_ch_layout:表示声道也可以是布局(扬声器的布局)AV_CH_LAYOUT_STEREO  立体声;
    - out_sample_fmt:输出的采样格式 16 = AV_SAMPLE_FMT_S16 或者 32 =  AV_SAMPLE_FMT_FLT;
    -  av_sample_fmt_s16 in_ch_layout:输入的声道布局  ;
    -  in_sample_fmt 输入的采样格式 ;
    -  in_sample_rate:  输入的采样率;
    -  后两位是log相关 0,null
    
    3. 初始化重采样
    - swr_init 初始化上下文  
    
    4. 进行重采样
    - swr_convert 开始转换 ,目的就是将输入缓冲区的数据写入输出缓冲区
      out:输出结果缓冲区 out_count:每个通道的采样数 
      in:输入的缓冲区 in_count:输入的单个通道的采样数  
    - 因为重采样的数据需要重新构造所以需要创建输入缓冲区和输出缓冲区  
      使用av_sample_array_and_samples audio_data创建
      其中的单通道采样数(单位是字节)`nb_samples = pkt.size / (32位 / 8) / 2(通道数)` 
      linessize:缓冲区大小  align:对齐 0 
    - 在转换前需要将pkt的data按字节拷贝到输入缓冲区,调用memcpy需要引用string.h
    
    - 将输出数据写入文件
      将输出缓冲区已经转换的数据写入文件
    
    5. 释放资源
    - 还有输入输出缓冲区的释放av_freep
    - swr_free释放上下文
    

    重采样上下文初始化代码

    SwrContext * init_swr_context(void) {
        
        SwrContext *context = NULL;
        
        // 假设已经提前知道输入音频数据的三要素的值 AV_CH_LAYOUT_STEREO, AV_SAMPLE_FMT_FLT, 44100
        context = swr_alloc_set_opts(NULL,
                                     AV_CH_LAYOUT_STEREO,
                                     AV_SAMPLE_FMT_S16,
                                     44100,
                                     AV_CH_LAYOUT_STEREO,
                                     AV_SAMPLE_FMT_FLT,
                                     44100,
                                     0, NULL);
        
        int result = swr_init(context);
        
        if (result != 0) {
            char error[1024];
            av_make_error_string(error, 1024, result);
            printf("初始化重采样上下文失败:%s", error);
        }
        
        return context;
    }
    

    将采集的数据重采样后 写入文件代码

    void get_audio_packet(AVFormatContext *context, void (*packet_callback)(AVPacket)) {
        
        // w == 写  b == 二进制  + == 没有就创建文件
        FILE *f = fopen("/Users/cunw/Desktop/learning/音视频学习/音视频文件/resample.pcm", "wb+");
    
        // 初始化重采样上下文
        SwrContext *swr_context = init_swr_context();
    
        // 初始化转换的输入输出缓冲区
        uint8_t **out_buffer = NULL;
        int linesize_out = 0;
        av_samples_alloc_array_and_samples(&out_buffer, &linesize_out, 2, 512, AV_SAMPLE_FMT_S16, 0);
        uint8_t **in_buffer = NULL;
        int linesize_in = 0;
        // nb_samples 单通道采样数 4096 / (32 / 8) / 2 = 1024
        av_samples_alloc_array_and_samples(&in_buffer, &linesize_in, 2, 512, AV_SAMPLE_FMT_FLT, 0);
    
    
        AVPacket *packet = av_packet_alloc();
        int result = -1;
        // 循环读取设备信息
        // result == -35 是Resource temporarily unavailable 因为获取太频繁 设备未准备好,还正在处理数据
        sleep(1);
        while (((result = av_read_frame(context, packet)) == 0  || result == -35) && isRecording == 1) {
    
            if (packet->size > 0) {
    
    
                // 开始转换数据
                // 先将音频数据拷贝到输入缓冲区  只是重采样音频的话  只需要处理数组的第一个
                memcpy(in_buffer[0], packet->data, packet->size);
                // 再进行转换
                swr_convert(swr_context, out_buffer, 512, (const uint8_t **)in_buffer, 512);
    
                fwrite(out_buffer[0],linesize_out, 1, f);
                // 每读取一次 就清空数据包 不然数据包会一直增大
                av_packet_unref(packet);
            }
    
        }
    
        if (result != 0) {
            char errors[1024];
            av_make_error_string(errors, 1024, result);
    
            printf("get packet occured error is \"%s\" \n", errors);
        }
    
        // 释放重采样资源
        if (in_buffer) {
            av_freep(&in_buffer[0]);
        }
        if (out_buffer) {
            av_freep(&out_buffer[0]);
        }
        av_freep(&in_buffer);
        av_freep(&out_buffer);
        swr_free(&swr_context);
    
        // 将缓冲区剩余的数据 强制写入文件
        fflush(f);
        fclose(f);
    
        // 释放packet空间
        av_packet_free(&packet);
        
        
    }
    

    ffmpeg 音频数据编码

    在使用fdk_aac编码器的时候,由于默认的ffmpeg有自带的aac,所以通过avcodec_find_encoder_by_name("libfdk_aac")就获取不到。在编译的时候加上--enable-libfdk-aac。注意:重新编译安装ffmpeg之前最好先删掉之前的ffmpeg,然后更新项目中的动态库;
    如果还不行,试试单独下载安装[fdk_aac](https://www.linuxfromscratch.org/blfs/view/svn/multimedia/fdk-aac.html),再重新编译ffmpeg

    1. 创建编码器 avcodec

      1. avcodec_find_encoder 一种通过名字查找 一种是通过id查找,id的查找方式只会找默认的编码器,比如aac,如果是fdkaac就需要通过名字查找;
      • AV_CODEC_ID_AAC | opus 其他编码器

      • "libfdk_aac", aac默认的规格是AAC LC

    2. 创建上下文 avcodexcontext
      设置音频三要素

      • avcodec_alloc_context3
        3表示第三个版本

      • sample_fmt = av_sample_FMT_S16 aac编码器不支持flt 32位

      • chnnel_layout = AV_CH_LAYOUT_STEREO( 或者chanels = 2)

      • sample_rate = 44100

      • bit_rate = 64000; (KB 码率)可选设置

      • profile = FF_PROFILE_AAC_HE_V2; (只有bit_rate=0 才有用) 可选设置,设置编码器规格

    3. 打开编码器

      • avcodex_opne2
        2表示第二个版本
        送数据给编码器时,编码器内部有一个缓冲区,缓冲一部分数据后才进行编码
    1. 编码

      • 用AVFrame包装未编码的数据,相当于是个输入,用AVPacket包装已编码的数据,相当于是个输出;

      • 调用avcodec_send_frame 将avframe缓冲区的数据发送给编码器,如果返回值大于0,就表示数据成功发送到了编码器,接着就可以通过循环使用 avcodec_receive_packet读取编码好的数据到AVPacket,并写入文件中,如果读取的结果是AVERROR(EAGIN)或者是AVERROR_EOF,就停止读取,如果是其他的负数,就停止编码;

      • av_frame_alloc 堆区初始化frame

      设置frame的nb_samples 单通道一个数据帧采样数 512

      format 每个采样的大小 av_sample_fmt_s16

      channel_layout 声道 av_ch_layout_stereo

      • av_frame_get_buffer 分配frame里面buffer的大小

      还要判断frame的buffer是否分配成功

      • 将重采样后的数据memcpy到frame->data中

      • 再将frame中的数据塞到编码器上下文中 avcodec_send_frame,该函数会返回一个int , 当结果>=0的时候表明有数据已经在编码缓冲区了;

      • avcodec_receive_packet 读取编码好的数据 avpacket

      • av_packet_alloc 分配编码后的数据空间

      因为编码器上下文中有一个缓冲区,其中会缓存多个frame,因此并不是每塞一个frame就会有一个packet出来,所以需要通过一个while循环判断编码器的数据是否>=0,再通过avcodec_receive_packet获取packet,该函数也会返回一个int,如果返回值>=0表明获取成功,如果失败直接退出编码,这个值返回值还有其他含义,需要判断eagain 表明编码器没有数据了或者是有数据但是不够编码 这个eagain需要用AVERROR包装成一个负数,表明数据还没准备好 averror_eof 表明一点数据都没有了;

      • 最后将数据编码后的数据写入到文件pkt->data,数据格式就是aac了;

      在停止录制的时候,由于编码的缓存区可能还有数据,在最后关闭之前,再去取一遍编码数据放入文件;

    1. 释放资源

    在结束的时候释放frame(av_frame_free) 和packet(av_packet_frame);

    编码实战代码:

    1. 创建fdk_aac编码器及上下文

    AVCodecContext* init_codec_context(void) {
        
        // 创建aac编码器
        AVCodec *codec = avcodec_find_encoder_by_name("libfdk_aac");
        
        // 初始化上下文
        AVCodecContext *context = NULL;
        context = avcodec_alloc_context3(codec);
        context->sample_fmt = AV_SAMPLE_FMT_S16;
        context->sample_rate = 44100;
        context->channel_layout = AV_CH_LAYOUT_STEREO;
        context->bit_rate = 0;
        // bitrate == 0 才会生效
        context->profile = FF_PROFILE_AAC_HE_V2;
        
        int result = avcodec_open2(context, codec, NULL);
        if (result < 0) {
            char error[1024];
            av_make_error_string(error, 1024, result);
            av_log(NULL, AV_LOG_DEBUG, "创建AAC编码器失败:%s",error);
        }
        
        return context;
        
    }
    

    2. 创建输入缓冲区

    AVFrame* create_audio_input_frame(void) {
        
        AVFrame *codec_frame = NULL;
        codec_frame = av_frame_alloc();
        
        codec_frame->nb_samples = 512;
        codec_frame->channel_layout = AV_CH_LAYOUT_STEREO;
        codec_frame->format = AV_SAMPLE_FMT_S16;
        int buffer_result = av_frame_get_buffer(codec_frame, 0);
        if (buffer_result < 0) {
            char error[1024];
            av_make_error_string(error, 1024, buffer_result);
            printf("frame 缓冲区分配失败:%s", error);
        }
        
        return codec_frame;
    }
    

    3. 开始编码并写入文件

    void audio_encode(AVCodecContext *ctx, AVFrame *frame, AVPacket *packet, FILE *fl) {
        
        
        // 将数据送入编码器
        int codec_result = avcodec_send_frame(ctx, frame);
        while (codec_result >= 0) {
            // 从packet中循环读取编码好的数据
            codec_result = avcodec_receive_packet(ctx, packet);
            if (codec_result == AVERROR(EAGAIN) || codec_result == AVERROR_EOF) {
            
                break;
            } else if (codec_result < 0) {
                char error[1024];
                av_make_error_string(error, 1024, codec_result);
                printf("编码器出错:%s     停止编码", error);
            } else {
                fwrite(packet->data, 1,packet->size, fl);
            }
        }
        if (codec_result < 0) {
            char error[1024];
            av_make_error_string(error, 1024, codec_result);
            printf("将数据送入编码器错误: %s\n",error);
        }
    }
    

    4. 调用

    • 先将重采样的数据放入avframe的缓冲区中
    memcpy(codec_frame->data[0], out_buffer[0], linesize_out);
    
    • 再开始编码
    audio_encode(codec_context, codec_frame, codec_packet, f);
    
    • 总览
    void get_audio_packet(AVFormatContext *context, void (*packet_callback)(AVPacket)) {
        
        // w == 写  b == 二进制  + == 没有就创建文件
        FILE *f = fopen("/Users/cunw/Desktop/learning/音视频学习/音视频文件/encoder.aac", "wb+");
        
        // 创建编码器上下文
        AVCodecContext *codec_context = init_codec_context();
        // 初始化输入缓冲区  AVframe
        AVFrame *codec_frame = create_audio_input_frame();
        // 初始化编码输出缓冲区
        AVPacket *codec_packet = av_packet_alloc();
        
        // 初始化重采样上下文
        SwrContext *swr_context = init_swr_context();
        // 初始化重采样的缓冲区
        uint8_t **out_buffer = NULL;
        int linesize_out = 0;
        uint8_t **in_buffer = NULL;
        int linesize_in = 0;
        init_resammple_buffer(&in_buffer, &linesize_in, &out_buffer, &linesize_out);
        
    
        AVPacket *packet = av_packet_alloc();
        int result = -1;
        // 循环读取设备信息
        
        while (isRecording == 1) {
            
            result = av_read_frame(context, packet);
            if (packet->size > 0 && result == 0) {
    
                packet_callback(*packet);
                // 开始转换数据
                // 先将音频数据拷贝到输入缓冲区  只是重采样音频的话  只需要处理数组的第一个
                memcpy(in_buffer[0], packet->data, packet->size);
                // 再进行转换
                swr_convert(swr_context, out_buffer, 512, (const uint8_t **)in_buffer, 512);
                // 将重采样好的数据按字节拷贝到frame缓冲区
                memcpy(codec_frame->data[0], out_buffer[0], linesize_out);
                audio_encode(codec_context, codec_frame, codec_packet, f);
                // 每读取一次 就清空数据包 不然数据包会一直增大
                av_packet_unref(packet);
            } else if (result == -EAGAIN) {
                // result == -35 是Resource temporarily unavailable 因为设备未准备好,还正在处理数据
                av_usleep(1);
            }
        }
        // 把缓冲区剩余的数据拿出来编码
        audio_encode(codec_context, NULL, codec_packet, f);
    
        if (result != 0) {
            char errors[1024];
            av_make_error_string(errors, 1024, result);
            printf("get packet occured error is \"%s\" \n", errors);
        }
    
        // 释放重采样资源
        if (in_buffer) {
            av_freep(&in_buffer[0]);
        }
        if (out_buffer) {
            av_freep(&out_buffer[0]);
        }
        av_freep(&in_buffer);
        av_freep(&out_buffer);
        swr_free(&swr_context);
        
        av_frame_free(&codec_frame);
        av_packet_free(&codec_packet);
    
        // 将缓冲区剩余的数据 强制写入文件
        fflush(f);
        fclose(f);
    
        // 释放packet空间
        av_packet_free(&packet);
        
        
    }
    

    上一篇:音频编码原理

    相关文章

      网友评论

          本文标题:4. 【音频编解码实战】

          本文链接:https://www.haomeiwen.com/subject/gsuvertx.html