使用FFmpeg API转音频格式

最近需要用到一款能自由转换音频格式的应用,所以就想到了大名鼎鼎的FFmpeg。

但是如果只使用编译好的exe执行程序来做,对于一个代码工作者来说又显得低端了。所以就抽时间研究了一下FFmpeg的代码逻辑。

同时因为相关的api应用的文章相对较少,看了不少FFmpeg的文档后,终于写了这些代码,然后就也尝试着粗浅地写一下如何使用FFmpeg api来转换音频代码格式。

其实是最近深感某狗音乐的流氓逻辑,明明已经是会员,下载音乐却是经过本地加密的文件,无法复制到其他地方播放,尤其是车机上。

所以想尝试解密音乐信息,然后转换成mp3格式,这样就可以放到车机上肆意播放了。

当然了,破解人家加密的事情,这里就不公开说了,今天只讲使用FFmpeg api来转换音乐格式的内容。

原理

其实基本的原因就是解码再编码,想想也挺简单。

但如何把我们平时觉得不太具象化的音频转换成FFmpeg对象,然后再一步步地去解码,编码,这才是今天的重点。

读取音频文件

c 复制代码
// 打开需要转换格式的音频文件
static int open_input_file(const char *filename,
                           AVFormatContext **input_format_context,
                           AVCodecContext **input_codec_context)
{
    AVCodecContext *avctx;
    const AVCodec *input_codec;
    const AVStream *stream;
    int error;

    /* 读取文件内容 */
    if ((error = avformat_open_input(input_format_context, filename, NULL,
                                     NULL)) < 0) {
        fprintf(stderr, "Could not open input file '%s' (error '%s')\n",
                filename, av_err2str(error));
        *input_format_context = NULL;
        return error;
    }

    /* 获取音频信息, streams 等*/
    if ((error = avformat_find_stream_info(*input_format_context, NULL)) < 0) {
        fprintf(stderr, "Could not open find stream info (error '%s')\n",
                av_err2str(error));
        avformat_close_input(input_format_context);
        return error;
    }

    /* 确定是否仅有一个stream */
    if ((*input_format_context)->nb_streams != 1) {
        fprintf(stderr, "Expected one audio input stream, but found %d\n",
                (*input_format_context)->nb_streams);
        avformat_close_input(input_format_context);
        return AVERROR_EXIT;
    }

    stream = (*input_format_context)->streams[0];

    /* 获取输入音频的编码格式 */
    if (!(input_codec = avcodec_find_decoder(stream->codecpar->codec_id))) {
        fprintf(stderr, "Could not find input codec\n");
        avformat_close_input(input_format_context);
        return AVERROR_EXIT;
    }

    /* 创建一个新的编译context */
    avctx = avcodec_alloc_context3(input_codec);
    if (!avctx) {
        fprintf(stderr, "Could not allocate a decoding context\n");
        avformat_close_input(input_format_context);
        return AVERROR(ENOMEM);
    }

    /* 使用原文件的一些参数,初始化新文件的参数*/
    error = avcodec_parameters_to_context(avctx, stream->codecpar);
    if (error < 0) {
        avformat_close_input(input_format_context);
        avcodec_free_context(&avctx);
        return error;
    }

    /* 打开新文件,以待后面使用 */
    if ((error = avcodec_open2(avctx, input_codec, NULL)) < 0) {
        fprintf(stderr, "Could not open input codec (error '%s')\n",
                av_err2str(error));
        avcodec_free_context(&avctx);
        avformat_close_input(input_format_context);
        return error;
    }

    /* 设置时间线 */
    avctx->pkt_timebase = stream->time_base;

    /* 保存编码context , 以备后面更方便使用。*/
    *input_codec_context = avctx;
    return 0;
}

读取输出文件

c 复制代码
// 打开输入文件,获取需要的编码格式,并设置一些必要的参数
// 其中一些参数来源于输入文件
static int open_output_file(const char *filename,
                            AVCodecContext *input_codec_context,
                            AVFormatContext **output_format_context,
                            AVCodecContext **output_codec_context)
{
    AVCodecContext *avctx          = NULL;
    AVIOContext *output_io_context = NULL;
    AVStream *stream               = NULL;
    const AVCodec *output_codec    = NULL;
    int error;

    /* 打开输入文件 */
    if ((error = avio_open(&output_io_context, filename,
                           AVIO_FLAG_WRITE)) < 0) {
        fprintf(stderr, "Could not open output file '%s' (error '%s')\n",
                filename, av_err2str(error));
        return error;
    }

    /* 新创建一个AVFormatContext指针对象*/
    if (!(*output_format_context = avformat_alloc_context())) {
        fprintf(stderr, "Could not allocate output format context\n");
        return AVERROR(ENOMEM);
    }

    /* AVIOContext */
    (*output_format_context)->pb = output_io_context;

    /* 先盲猜一波目标对象的格式,编码信息,主要是根据存储格式。*/
    if (!((*output_format_context)->oformat = av_guess_format(NULL, filename,
                                                              NULL))) {
        fprintf(stderr, "Could not find output file format\n");
        goto cleanup;
    }

    if (!((*output_format_context)->url = av_strdup(filename))) {
        fprintf(stderr, "Could not allocate url.\n");
        error = AVERROR(ENOMEM);
        goto cleanup;
    }

    /* 这里用来确定存储文件的编码, 如aac/mp3 */
//    if (!(output_codec = avcodec_find_encoder(AV_CODEC_ID_AAC))) {
    if (!(output_codec = avcodec_find_encoder(AV_CODEC_ID_MP3))) {
        fprintf(stderr, "Could not find an MP3 encoder.\n");
        goto cleanup;
    }

    /* 创建一个新的音频流 */
    if (!(stream = avformat_new_stream(*output_format_context, NULL))) {
        fprintf(stderr, "Could not create new stream\n");
        error = AVERROR(ENOMEM);
        goto cleanup;
    }

    avctx = avcodec_alloc_context3(output_codec);
    if (!avctx) {
        fprintf(stderr, "Could not allocate an encoding context\n");
        error = AVERROR(ENOMEM);
        goto cleanup;
    }

    /* 设置一些基础的编码信息 */
    av_channel_layout_default(&avctx->ch_layout, OUTPUT_CHANNELS);
    avctx->sample_rate    = input_codec_context->sample_rate;
    avctx->sample_fmt     = output_codec->sample_fmts[0];
//    avctx->bit_rate       = OUTPUT_BIT_RATE;
    //和输入同样的码率
    avctx->bit_rate       = input_codec_context->bit_rate;

    /* 设置相同的采样率*/
    stream->time_base.den = input_codec_context->sample_rate;
    stream->time_base.num = 1;

    // 某些容器格式(如 MP4)需要全局头信息存在。
    // 输入文件的采样率用于避免进行采样率转换。
    if ((*output_format_context)->oformat->flags & AVFMT_GLOBALHEADER)
        avctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;

    /* 打开音频流的编码器,以便稍后使用。*/
    if ((error = avcodec_open2(avctx, output_codec, NULL)) < 0) {
        fprintf(stderr, "Could not open output codec (error '%s')\n",
                av_err2str(error));
        goto cleanup;
    }

    error = avcodec_parameters_from_context(stream->codecpar, avctx);
    if (error < 0) {
        fprintf(stderr, "Could not initialize stream parameters\n");
        goto cleanup;
    }

    /* 保存编码器上下文,以便日后更方便地访问。 */
    *output_codec_context = avctx;

    return 0;

    cleanup:
    avcodec_free_context(&avctx);
    avio_closep(&(*output_format_context)->pb);
    avformat_free_context(*output_format_context);
    *output_format_context = NULL;
    return error < 0 ? error : AVERROR_EXIT;
}

生成新音频

然后就基本可以使用这些代码去生成新音频了

c 复制代码
int main()
{
    av_log_set_level(AV_LOG_DEBUG);
//    avformat_network_init();
    AVFormatContext *input_format_context = NULL, *output_format_context = NULL;
    AVCodecContext *input_codec_context = NULL, *output_codec_context = NULL;
    SwrContext *resample_context = NULL;
    AVAudioFifo *fifo = NULL;
    int ret = AVERROR_EXIT;
    const char *file_name = "C:\\Users\\Administrator\\Desktop\\kugou\\kugou_music_convertor.ogg";
    const char *out_file_name = "C:\\Users\\Administrator\\Desktop\\kugou\\平凡日子里的挣扎_kugou_music_convertor.mp3";

    if (open_input_file(file_name, &input_format_context,
                        &input_codec_context))
        goto cleanup;

//    av_dump_format(input_format_context,0,file_name,0);   //打印输入文件细节

    if (open_output_file(out_file_name, input_codec_context,
                         &output_format_context, &output_codec_context))
        goto cleanup;

    if (set_metadata(input_format_context, &output_format_context) < 0)
    {
        av_log(NULL, AV_LOG_DEBUG, "cp metadata failed");
    }

    /* 初始化重采样器,使其能够转换音频样本格式。 */
    if (init_resampler(input_codec_context, output_codec_context,
                       &resample_context))
        goto cleanup;
    /* 初始化 FIFO 缓冲区,用于存储待编码的音频样本。*/
    if (init_fifo(&fifo, output_codec_context))
        goto cleanup;
    /* 编写输出文件容器的标题部分。 */
    if (write_output_file_header(output_format_context))
        goto cleanup;

    /* 循环,只要我们还有可读的输入样本或有待写入的输出样本,就一直循环;一旦既没有可读的输入样本也没有待写入的输出样本,就立即停止循环。 */
    while (1) {
        const int output_frame_size = output_codec_context->frame_size;
        int finished                = 0;

        /* 确保在 FIFO 缓冲区中有一帧的样本量,以便编码器能够完成其工作。
				* 由于解码器和编码器的帧大小可能不同,所以我们需要使用 FIFO 缓冲区来存储尽可能多的帧量的输入样本,
				* 以确保它们能生成至少一帧量的输出样本。 */
        while (av_audio_fifo_size(fifo) < output_frame_size) {
            /* 解码一帧音频样本,将其转换为*output 样本格式,并将其放入 FIFO 缓冲区。*/
            if (read_decode_convert_and_store(fifo, input_format_context,
                                              input_codec_context,
                                              output_codec_context,
                                              resample_context, &finished))
                goto cleanup;

            /* 如果当前处于输入文件的末尾,我们将继续将剩余的音频样本编码到输出文件中。 */
            if (finished)
                break;
        }

        /* 如果编码器有足够的样本,就对其进行编码。在文件的末尾,将剩余的样本传递给编码器。*/
        while (av_audio_fifo_size(fifo) >= output_frame_size ||
               (finished && av_audio_fifo_size(fifo) > 0))
            /* 从 FIFO 缓冲区中选取一帧音频样本,对其进行编码,并将其写入输出文件中。*/
            if (load_encode_and_write(fifo, output_format_context,
                                      output_codec_context))
                goto cleanup;

        /* 如果当前已到达输入文件的末尾,并且已经对所有剩余的样本进行了编码,那么我们就可以退出这个循环并完成操作。 */
        if (finished) {
            int data_written;
            /* 请清除编码器中的缓存,因为它可能存有延迟的帧。 */
            do {
                if (encode_audio_frame(NULL, output_format_context,
                                       output_codec_context, &data_written))
                    goto cleanup;
            } while (data_written);
            break;
        }
    }

    /* 为输出文件容器编写封装文件的引导信息。 */
    if (write_output_file_trailer(output_format_context))
        goto cleanup;
    ret = 0;

    cleanup:
    if (fifo)
        av_audio_fifo_free(fifo);
    swr_free(&resample_context);
    if (output_codec_context)
        avcodec_free_context(&output_codec_context);
    if (output_format_context) {
        avio_closep(&output_format_context->pb);
        avformat_free_context(output_format_context);
    }
    if (input_codec_context)
        avcodec_free_context(&input_codec_context);
    if (input_format_context)
        avformat_close_input(&input_format_context);

    return ret;
}

更多信息

FFmpeg源码:

https://github.com/FFmpeg/FFmpeg

FFmpeg 文档:

https://ffmpeg.org/documentation.html

调用API需要的动态库(注意相关的开源协议):

https://github.com/BtbN/FFmpeg-Builds

相关推荐
Vincent_Vang3 小时前
多态 、抽象类、抽象类和具体类的区别、抽象方法和具体方法的区别 以及 重载和重写的相同和不同之处
java·开发语言·前端·ide
i757_w4 小时前
IDEA快捷键被占用
java·ide·intellij-idea
讲师-汪春波5 小时前
win11 vscode 配置 claude code
ide·vscode·编辑器
小馬佩德罗5 小时前
如何将x264 x265的动态库编译入Linux系统中的FFmpeg源码 - FFmpeg编译
linux·ffmpeg
神气龙6 小时前
VS Code连接wsl上Conda虚拟环境,打开Jupyter Notebook
ide·python·jupyter
你好音视频6 小时前
FFmpeg HLS编码流程深度解析:从数据包到播放列表的完整实现
c++·ffmpeg·音视频
芒鸽6 小时前
macOS 上用 lycium 交叉编译 FFmpeg 适配鸿蒙(OHOS):从构建到 HNP 打包
macos·ffmpeg·harmonyos
爱吃泡芙的小白白21 小时前
如何在现有配置好环境的Pycharm中安装jupyterlab这个工具
ide·python·pycharm·notebook·虚拟环境·jupyterlab