最近需要用到一款能自由转换音频格式的应用,所以就想到了大名鼎鼎的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需要的动态库(注意相关的开源协议):