音视频开发9. 使用ffmpeg 将pcm转码mp3实践(C++)

@[TOC](音视频开发9. 使用ffmpeg 将pcm转码mp3实践(C++))

一、准备环境

  • CentOS环境
  • 安装 ffmpeg 库,并有必要的库(主要是lame:mp3解码库)
  • ffmpeg库安装可参考之前文档

二、一些预备知识

1. 音频格式说明

如前文所述,pcm是音频裸数据,要转成mp3需要进行重采样、编码步骤。常见的PCM格式有8位和16位两种。

  • 8位每一个PCM数据的值由一个字节即8位来表示(0-255)
  • 16位是指每一个PCM数据的值由两个字节即16位来表示,分为高8位和第8位(-32767~32767)

2. 采样频率

采样频率指每秒钟对音频的采样点数,单位为Hz(赫兹)。
如采样频率为44100hz是指每秒钟采集44100个样本点。

3. 声道数

常见的声道数有:

  • 单声道:mono
  • 双声道:stereo,包含左右两声道
  • 2.1声道:在双声道基础上增加了一个低音声道
  • 5.1声道:分别为正面、左前方、右前方、左环绕、右环绕声道、一个低音声道
  • 7.1声道:在5.1声道的基础上,把左右的环绕声道拆分为左右环绕声道以及左右后置声道,主要应用于BD以及现代的电影院

4. 样本大小

例:

  • 1024个16位单声道PCM样本,它的样本大小为102421=2048字节;
  • 1024个16位双声道PCM样本,它的样本大小为102422=4096字节

5. 一帧样本数

  • PCM 一般为1024;
  • MP3 一般为 1152。

6. 参考命令行

ffmpeg -y -ac 1 -ar 16000 -f s16le -i /data/ffmpeg/test/input.pcm -c:a libmp3lame -q:a 2 /data/ffmpeg/test/output.mp3

三、几个重要函数

1. 重采样参数设置两个函数

3.1.1 swr_alloc_set_opts

示例:

  SwrContext* swrContext = NULL;
  // 设置参数, 1. 重采样上下文 2.输出声道布局 4.输出采样率, 5.输入声道布局 6.输入样本格式 7.输入采样率 8.配音 9.日志
  swrContext = swr_alloc_set_opts(swrContext, avCodecContext->channel_layout, avCodecContext->sample_fmt, avCodecContext->sample_rate,
    AV_CH_LAYOUT_STEREO, AV_SAMPLE_FMT_S16, 44100,
    0, 0);

3.1.2 av_opt_set_int

示例:

    SwrContext *swrContext = swr_alloc();
    // 通道布局:立体声
    av_opt_set_int(swrContext, "in_channel_layout", AV_CH_LAYOUT_STEREO, 0);
    // 采样率:44100
    av_opt_set_int(swrContext, "in_sample_rate", OSR, 0);
    //  样本格式 s16交错存储
    av_opt_set_sample_fmt(swrContext, "in_sample_fmt", AV_SAMPLE_FMT_S16, 0);

在设置完参数后,要调用swr_init进行初始化。

2. 分配样本数据内存空间

3.2.1 av_samples_alloc_array_and_samples

根据音频格式分配相应大小的内存空间,函数内部会调用 av_samples_alloc ,示例代码:

    result = av_samples_alloc_array_and_samples(&input_data, &input_linesize, 2, avFrame->nb_samples, AV_SAMPLE_FMT_S16, 0);

3.2.2 av_samples_alloc

根据音频格式分配相应大小的内存空间。用于转换过程中对输出内存大小进行调整。

3. 整体流程

在这里插入图片描述

四、实现代码

1. CMakeLists.txt

cmake_minimum_required(VERSION 3.17)
project(ffmpeg_demo)

# 设置ffmpeg依赖库及头文件所在目录,并存进指定变量
set(ffmpeg_libs_DIR /home/xundh/ffmpeg_sources/ffmpeg-4.2.2)
set(ffmpeg_headers_DIR /home/xundh/ffmpeg_sources/ffmpeg-4.2.2)

#对于find_package找不到的外部依赖库,可以用add_library添加
# SHARED表示添加的是动态库
# IMPORTED表示是引入已经存在的动态库

add_library( avcodec SHARED IMPORTED)
add_library( avfilter SHARED IMPORTED )
add_library( swresample SHARED IMPORTED )
add_library( swscale SHARED IMPORTED )
add_library( avformat SHARED IMPORTED )
add_library( avutil SHARED IMPORTED )


#指定所添加依赖库的导入路径
set_target_properties( avcodec PROPERTIES IMPORTED_LOCATION ${
    
    ffmpeg_libs_DIR}/libavcodec/libavcodec.so )
set_target_properties( avfilter PROPERTIES IMPORTED_LOCATION ${
    
    ffmpeg_libs_DIR}/libavfilter/libavfilter.so )
set_target_properties( swresample PROPERTIES IMPORTED_LOCATION ${
    
    ffmpeg_libs_DIR}/libswresample/libswresample.so )
set_target_properties( swscale PROPERTIES IMPORTED_LOCATION ${
    
    ffmpeg_libs_DIR}/libswscale/libswscale.so )
set_target_properties( avformat PROPERTIES IMPORTED_LOCATION ${
    
    ffmpeg_libs_DIR}/libavformat/libavformat.so )
set_target_properties( avutil PROPERTIES IMPORTED_LOCATION ${
    
    ffmpeg_libs_DIR}/libavutil/libavutil.so )


# 添加头文件路径到编译器的头文件搜索路径下,多个路径以空格分隔
include_directories( ${
    
    ffmpeg_headers_DIR} )
link_directories(${
    
    ffmpeg_libs_DIR} )
link_directories(/usr/lib)


set(CMAKE_CXX_STANDARD 14)
# add_executable(ffmpeg_demo main.cpp)
add_executable(ffmpeg_demo pcm_to_mp3.cpp)
target_link_libraries(${
    
    PROJECT_NAME}  avcodec avformat avutil swresample swscale swscale avfilter )

2. 主文件

#include <iostream>

#ifdef __cplusplus
extern "C"
{
    
    
#endif
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libavutil/imgutils.h"
#include "libavutil/log.h"
#include "libswresample/swresample.h"

#include "libavutil/avutil.h"
#include "libavutil/opt.h"
#ifdef __cplusplus
}
#endif
using namespace std;

#define CHANNEL 2
#define OSR 44100

/**
 * pcm 转 mp3格式,输入文件路径
 */
int pcm_to_mp3(const char *pcm_file_path, const char *mp3_file_path)
{
    
    
    FILE *pcm_file = NULL;
    FILE *mp3_file = NULL;
    int result;

    // 获取mp3编码器
    cout << "获取mp3编码器" << endl;
    const AVCodec *avCodec = avcodec_find_encoder(AV_CODEC_ID_MP3);
    if (!avCodec)
    {
    
    
        cout << "初始化mp3 编码器失败" << endl;
        return -1;
    }

    // 创建编码器上下文
    AVCodecContext *avCodecContext = avcodec_alloc_context3(avCodec);
    if (!avCodecContext)
    {
    
    
        cout << "avcodec_alloc_context3 失败" << avCodecContext << endl;
        return -1;
    }
    avCodecContext->bit_rate = 64000;
    avCodecContext->channels = CHANNEL;
    avCodecContext->channel_layout = AV_CH_LAYOUT_STEREO;
    avCodecContext->sample_rate = OSR;
    avCodecContext->sample_fmt = AV_SAMPLE_FMT_S16P;
    avCodecContext->time_base = av_get_time_base_q();

    // 打开编码器
    cout << "打开mp3编码器" << endl;
    result = avcodec_open2(avCodecContext, avCodec, NULL);
    if (result < 0)
    {
    
    
        cout << "avcodec_open2失败: " << result << endl;
        return result;
    }

    cout << "打开mp3文件" << mp3_file_path << endl;
    // 打开输出文件
    mp3_file = fopen(mp3_file_path, "wb");
    if (!mp3_file)
    {
    
    
        cout << "打开mp3文件失败" << endl;
        return -1;
    }

    // AVFrame 接受重采样的每一帧的音频数据 每帧的样本大小为1152
    AVFrame *avFrame = av_frame_alloc();
    if (!avFrame)
    {
    
    
        cout << "分配avFrame帧失败" << endl;
        return -1;
    }
    // mp3一帧的样本数为1152
    avFrame->nb_samples = 1152;
    avFrame->channels = CHANNEL;
    avFrame->channel_layout = AV_CH_LAYOUT_STEREO;
    avFrame->format = AV_SAMPLE_FMT_S16P;

    // 给帧分配内存空间
    result = av_frame_get_buffer(avFrame, 0);
    if (result < 0)
    {
    
    
        cout << "分配帧内存失败" << endl;
        return result;
    }

    // 重采样  创建音频重采样上下文
    cout << "配置重采样器上下文" << endl;
    SwrContext *swrContext = swr_alloc();
    if (!swrContext)
    {
    
    
        cout << "配置重采样上下文失败" << endl;
        return -1;
    }

    // 设置重采样输入pcm参数:通道布局:立体声 采样率:44100  样本格式 s16交错存储
    av_opt_set_int(swrContext, "in_channel_layout", AV_CH_LAYOUT_STEREO, 0);
    av_opt_set_int(swrContext, "in_sample_rate", OSR, 0);
    av_opt_set_sample_fmt(swrContext, "in_sample_fmt", AV_SAMPLE_FMT_S16, 0);

    // 设置重采样输出mp3参数:通道布局:立体声 采样率:44100  样本格式 s16平面存储
    av_opt_set_int(swrContext, "out_channel_layout", AV_CH_LAYOUT_STEREO, 0);
    av_opt_set_int(swrContext, "out_sample_rate", OSR, 0);
    av_opt_set_sample_fmt(swrContext, "out_sample_fmt", AV_SAMPLE_FMT_S16P, 0);

    // 重采样初始化
    result = swr_init(swrContext);
    if (result < 0)
    {
    
    
        cout << "重采样器初始化失败,error=" << result << endl;
        return result;
    }

    uint8_t **input_data = NULL;
    uint8_t **output_data = NULL;
    int input_linesize, output_linesize;

    // 打开pcm文件
    cout << "打开源 pcm 文件 " << pcm_file_path << endl;
    pcm_file = fopen(pcm_file_path, "rb");
    if (!pcm_file)
    {
    
    
        cout << "打开 pcm 文件失败" << endl;
        return -1;
    }

    cout << "开始编码转换" << endl;

    // 给pcm文件数据分配空间
    result = av_samples_alloc_array_and_samples(&input_data, &input_linesize, 2, avFrame->nb_samples, AV_SAMPLE_FMT_S16, 0);
    if (result < 0)
    {
    
    
        cout << "给pcm文件分配空间失败, result = " << result << endl;
        return result;
    }

    // 缓存重采样数据的空间分配
    result = av_samples_alloc_array_and_samples(&output_data, &output_linesize, 2, avFrame->nb_samples, AV_SAMPLE_FMT_S16P, 0);
    if (result < 0)
    {
    
    
        cout << "获取mp3 重采样数据失败, result=" << result << endl;
        return result;
    }

    // 存放编码后的数据
    AVPacket *avPacket = av_packet_alloc();
    if (!avPacket)
    {
    
    
        cout << "分配 avPacket 内存失败" << endl;
        return -1;
    }

    cout << "==========循环读入帧==========" << endl;
    long total_size = 0;
    while (!feof(pcm_file))
    {
    
    
        long read_size = (long)fread(input_data[0], 1, avFrame->nb_samples * 4, pcm_file);
        total_size += read_size;
        if ((total_size / read_size) % 50 == 0)
        {
    
    
            cout << "读取数据:" << read_size << "字节; 累计:" << total_size << " 字节 " << endl;
        }

        if (read_size <= 0)
        {
    
    
            break;
        }

        // 重采样
        result = swr_convert(swrContext, output_data, avFrame->nb_samples, (const uint8_t **)input_data, avFrame->nb_samples);
        if (result < 0)
        {
    
    
            cout << "音频编码失败,错误信息" << result << endl;
            return result;
        }
        // 将重采样后的数据存入frame,MP3是s16p 先存放左声道的数据 后存放右声道的数据, data[0]是左声道,1是右声道
        avFrame->data[0] = output_data[0];
        avFrame->data[1] = output_data[1];

        // 编码,写入mp3文件,实际上是对frame这个结构体里面的数据进行编码操作,发送到编码线程:使用编码器  和 存储数据的frame
        result = avcodec_send_frame(avCodecContext, avFrame);
        if (result < 0)
        {
    
    
            cout << "mp3编码失败,错误信息:" << result << endl;
            return result;
        }

        while (result >= 0)
        {
    
    
            // 接收编码后的数据,使用编码器 和 存储编码数据的pkt, 有可能需要多次才能接收完成
            result = avcodec_receive_packet(avCodecContext, avPacket);

            // AVERROR_EOF表示没有数据了 这两个错误不影响继续接收数据
            if (result == AVERROR_EOF || result == AVERROR(EAGAIN))
            {
    
    
                continue;
            }
            else if (result < 0)
            {
    
    
                break;
            }
            fwrite(avPacket->data, 1, avPacket->size, mp3_file);
            av_packet_unref(avPacket);
        }
    }
    // 告诉解码器没有帧了,如果没有这几行的逻辑,在关闭 avCodecContext 可能会提示 * fames left in the queu on closing
    avcodec_send_frame(avCodecContext, __null);
    while(avcodec_receive_packet(avCodecContext, avPacket)!=AVERROR_EOF);

    // 关闭缓存
    if (input_data)
    {
    
    
        av_free(input_data);
    }
    if (output_data)
    {
    
    
        av_free(output_data);
    }
    cout << "关闭文件" << endl;
    fclose(pcm_file);
    fclose(mp3_file);
    cout << "释放资源" << endl;
    // s释放 frame pkt
    av_frame_free(&avFrame);
    av_packet_free(&avPacket);
    // 释放重采样上下文
    swr_free(&swrContext);
    // 释放编码器上下文
    avcodec_free_context(&avCodecContext);

    cout << "转码完成" << endl;

    return 0;
}

int main(int argc, char *argv[])
{
    
    
    const char *input = "input.pcm";
    const char *output = "out.mp3";

    pcm_to_mp3(input, output);
    return 1;
}

程序执行结果:

./ffmpeg_demo 
获取mp3编码器
打开mp3编码器
打开mp3文件out.mp3
配置重采样器上下文
打开源 pcm 文件 input.pcm
开始编码转换
==========循环读入帧==========
读取数据:4608字节; 累计:230400 字节 
读取数据:4608字节; 累计:460800 字节 
读取数据:4608字节; 累计:691200 字节 
读取数据:4608字节; 累计:921600 字节 
关闭文件
释放资源
转码完成

猜你喜欢

转载自blog.csdn.net/xundh/article/details/123257380