C++ FFmpeg 音频转换时声音失真

5
我正在使用FFmpeg库生成包含来自各种文件(如MP3,WAV,OGG)的音频的MP4文件,但我遇到了一些问题(为简化起见,此问题不考虑视频部分,因为我已经在那方面做得很好)。我的当前代码打开一个音频文件,解码内容并将其转换为MP4容器,最后将其作为交错帧写入目标文件。对于大多数MP3文件,它都能完美地工作,但是当输入WAV或OGG时,结果MP4中的音频会略微失真,并且经常以错误的速度播放(速度可能比实际快或慢许多倍)。我已经查看了无数使用转换函数(swr_convert)的示例,但似乎无法消除导出音频中的噪声。以下是我如何将音频流添加到MP4中(outContext是输出文件的AVFormatContext):
audioCodec = avcodec_find_encoder(outContext->oformat->audio_codec);
if (!audioCodec)
    die("Could not find audio encoder!");


// Start stream
audioStream = avformat_new_stream(outContext, audioCodec);
if (!audioStream)
    die("Could not allocate audio stream!");

audioCodecContext = audioStream->codec;
audioStream->id = 1;


// Setup
audioCodecContext->sample_fmt = AV_SAMPLE_FMT_S16;
audioCodecContext->bit_rate = 128000;
audioCodecContext->sample_rate = 44100;
audioCodecContext->channels = 2;
audioCodecContext->channel_layout = AV_CH_LAYOUT_STEREO;


// Open the codec
if (avcodec_open2(audioCodecContext, audioCodec, NULL) < 0)
    die("Could not open audio codec");

要从MP3/WAV/OGG格式的声音文件中打开(使用文件名变量)...

// Create contex
formatContext = avformat_alloc_context();
if (avformat_open_input(&formatContext, filename, NULL, NULL)<0)
    die("Could not open file");


// Find info
if (avformat_find_stream_info(formatContext, 0)<0)
    die("Could not find file info");

av_dump_format(formatContext, 0, filename, false);


// Find audio stream
streamId = av_find_best_stream(formatContext, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
if (streamId < 0)
    die("Could not find Audio Stream");

codecContext = formatContext->streams[streamId]->codec;


// Find decoder
codec = avcodec_find_decoder(codecContext->codec_id);
if (codec == NULL)
    die("cannot find codec!");


// Open codec
if (avcodec_open2(codecContext, codec, 0)<0)
    die("Codec cannot be found");


// Set up resample context
swrContext = swr_alloc();
if (!swrContext)
    die("Failed to alloc swr context");

av_opt_set_int(swrContext, "in_channel_count", codecContext->channels, 0);
av_opt_set_int(swrContext, "in_channel_layout", codecContext->channel_layout, 0);
av_opt_set_int(swrContext, "in_sample_rate", codecContext->sample_rate, 0);
av_opt_set_sample_fmt(swrContext, "in_sample_fmt", codecContext->sample_fmt, 0);

av_opt_set_int(swrContext, "out_channel_count", audioCodecContext->channels, 0);
av_opt_set_int(swrContext, "out_channel_layout", audioCodecContext->channel_layout, 0);
av_opt_set_int(swrContext, "out_sample_rate", audioCodecContext->sample_rate, 0);
av_opt_set_sample_fmt(swrContext, "out_sample_fmt", audioCodecContext->sample_fmt, 0);

if (swr_init(swrContext))
    die("Failed to init swr context");

最后,要进行解码+转换+编码...
// Allocate and init re-usable frames
audioFrameDecoded = av_frame_alloc();
if (!audioFrameDecoded)
        die("Could not allocate audio frame");

audioFrameDecoded->format = fileCodecContext->sample_fmt;
audioFrameDecoded->channel_layout = fileCodecContext->channel_layout;
audioFrameDecoded->channels = fileCodecContext->channels;
audioFrameDecoded->sample_rate = fileCodecContext->sample_rate;

audioFrameConverted = av_frame_alloc();
if (!audioFrameConverted)
        die("Could not allocate audio frame");

audioFrameConverted->nb_samples = audioCodecContext->frame_size;
audioFrameConverted->format = audioCodecContext->sample_fmt;
audioFrameConverted->channel_layout = audioCodecContext->channel_layout;
audioFrameConverted->channels = audioCodecContext->channels;
audioFrameConverted->sample_rate = audioCodecContext->sample_rate;

AVPacket inPacket;
av_init_packet(&inPacket);
inPacket.data = NULL;
inPacket.size = 0;

int frameFinished = 0;

while (av_read_frame(formatContext, &inPacket) >= 0) {

        if (inPacket.stream_index == streamId) {

                int len = avcodec_decode_audio4(fileCodecContext, audioFrameDecoded, &frameFinished, &inPacket);

                if (frameFinished) {

                        // Convert

                        uint8_t *convertedData=NULL;

                        if (av_samples_alloc(&convertedData,
                                             NULL,
                                             audioCodecContext->channels,
                                             audioFrameConverted->nb_samples,
                                             audioCodecContext->sample_fmt, 0) < 0)
                                die("Could not allocate samples");

                        int outSamples = swr_convert(swrContext,
                                                     &convertedData,
                                                     audioFrameConverted->nb_samples,
                                                     (const uint8_t **)audioFrameDecoded->data,
                                                     audioFrameDecoded->nb_samples);
                        if (outSamples < 0)
                                die("Could not convert");

                        size_t buffer_size = av_samples_get_buffer_size(NULL,
                                                                        audioCodecContext->channels,
                                                                        audioFrameConverted->nb_samples,
                                                                        audioCodecContext->sample_fmt,
                                                                        0);
                        if (buffer_size < 0)
                                die("Invalid buffer size");

                        if (avcodec_fill_audio_frame(audioFrameConverted,
                                                     audioCodecContext->channels,
                                                     audioCodecContext->sample_fmt,
                                                     convertedData,
                                                     buffer_size,
                                                     0) < 0)
                                die("Could not fill frame");

                        AVPacket outPacket;
                        av_init_packet(&outPacket);
                        outPacket.data = NULL;
                        outPacket.size = 0;

                        if (avcodec_encode_audio2(audioCodecContext, &outPacket, audioFrameConverted, &frameFinished) < 0)
                                die("Error encoding audio frame");

                        if (frameFinished) {
                                outPacket.stream_index = audioStream->index;

                                if (av_interleaved_write_frame(outContext, &outPacket) != 0)
                                        die("Error while writing audio frame");

                                av_free_packet(&outPacket);
                        }
                }
        }
}

av_frame_free(&audioFrameConverted);
av_frame_free(&audioFrameDecoded);
av_free_packet(&inPacket);

我还尝试设置适当的pts值来处理传出的帧,但这似乎并没有影响声音质量。

我也不确定是否应该为转换后的数据分配空间,可以使用av_samples_alloc吗?avcodec_fill_audio_frame呢?我是否走在正确的轨道上?

非常感谢您的任何建议(如果需要,我也可以发送导出的MP4文件,让您听到扭曲的声音)。


请注意,此代码中使用的许多方法现已过时。 - vstrom coder
3个回答

13
if (avcodec_encode_audio2(audioCodecContext, &outPacket, audioFrameConverted, &frameFinished) < 0)
                die("Error encoding audio frame");
你似乎假设编码器会吃掉所有提交的样本——但它并不会这样做。它也不会在内部缓存它们。它将吃掉特定数量的样本(AVCodecContext.frame_size),其余的应该在下一次调用avcodec_encode_audio2()时重新提交。
[编辑]
好的,你修改后的代码更好了,但还不够完善。你仍然假设解码器会为每次调用avcodec_decode_audioN()(经过重新采样后)输出至少frame_size个样本(对于ogg来说确实如此),但这可能并不是情况。如果发生这种情况,你的avcodec_encode_audioN()调用将对一个不完整的输入缓冲区进行编码(因为你说它有frame_size个样本,但实际上并没有)。同样,你的代码也没有处理解码器输出比编码器预期的frame_size(例如10*frame_size)大得多的数字的情况,在这种情况下,你会遇到溢出——基本上你的1:1解码/编码映射是问题的主要源头。
作为解决方案,请将swrContext视为一个FIFO,在其中输入所有解码器样本,并循环直到其剩余的样本少于frame_size个。关于如何处理流结束,我会让你自己学习,因为你需要清除解码器中缓存的样本(通过调用带有.data = NULL和.size = 0的AVPacket的avcodec_decode_audioN()),清除swrContext(通过调用swr_context()直到它返回0),以及清除编码器(通过提供NULL AVFrames直到它返回AVPacket与.size = 0)。现在你可能会得到一个输出文件,在结尾处稍微截断了一些。这应该不难解决。
对我来说,这个代码可以将m4a/ogg/mp3转换为m4a/aac:
#include "libswresample/swresample.h"
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libavutil/opt.h"

#include <stdio.h>
#include <stdlib.h>

static void die(char *str) {
    fprintf(stderr, "%s\n", str);
    exit(1);
}

static AVStream *add_audio_stream(AVFormatContext *oc, enum AVCodecID codec_id)
{
    AVCodecContext *c;
    AVCodec *encoder = avcodec_find_encoder(codec_id);
    AVStream *st = avformat_new_stream(oc, encoder);

    if (!st) die("av_new_stream");

    c = st->codec;
    c->codec_id = codec_id;
    c->codec_type = AVMEDIA_TYPE_AUDIO;

    /* put sample parameters */
    c->bit_rate = 64000;
    c->sample_rate = 44100;
    c->channels = 2;
    c->sample_fmt = encoder->sample_fmts[0];
    c->channel_layout = AV_CH_LAYOUT_STEREO;

    // some formats want stream headers to be separate
    if(oc->oformat->flags & AVFMT_GLOBALHEADER)
        c->flags |= CODEC_FLAG_GLOBAL_HEADER;

    return st;
}

static void open_audio(AVFormatContext *oc, AVStream *st)
{
    AVCodecContext *c = st->codec;
    AVCodec *codec;

    /* find the audio encoder */
    codec = avcodec_find_encoder(c->codec_id);
    if (!codec) die("avcodec_find_encoder");

    /* open it */
    AVDictionary *dict = NULL;
    av_dict_set(&dict, "strict", "+experimental", 0);
    int res = avcodec_open2(c, codec, &dict);
    if (res < 0) die("avcodec_open");
}

int main(int argc, char *argv[]) {
    av_register_all();

    if (argc != 3) {
        fprintf(stderr, "%s <in> <out>\n", argv[0]);
        exit(1);
    }

    // Allocate and init re-usable frames
    AVCodecContext *fileCodecContext, *audioCodecContext;
    AVFormatContext *formatContext, *outContext;
    AVStream *audioStream;
    SwrContext *swrContext;
    int streamId;

    // input file
    const char *file = argv[1];
    int res = avformat_open_input(&formatContext, file, NULL, NULL);
    if (res != 0) die("avformat_open_input");
    res = avformat_find_stream_info(formatContext, NULL);
    if (res < 0) die("avformat_find_stream_info");
    AVCodec *codec;
    res = av_find_best_stream(formatContext, AVMEDIA_TYPE_AUDIO, -1, -1, &codec, 0);
    if (res < 0) die("av_find_best_stream");
    streamId = res;
    fileCodecContext = avcodec_alloc_context3(codec);
    avcodec_copy_context(fileCodecContext, formatContext->streams[streamId]->codec);
    res = avcodec_open2(fileCodecContext, codec, NULL);
    if (res < 0) die("avcodec_open2");

    // output file
    const char *outfile = argv[2];
    AVOutputFormat *fmt = fmt = av_guess_format(NULL, outfile, NULL);
    if (!fmt) die("av_guess_format");
    outContext = avformat_alloc_context();
    outContext->oformat = fmt;
    audioStream = add_audio_stream(outContext, fmt->audio_codec);
    open_audio(outContext, audioStream);
    res = avio_open2(&outContext->pb, outfile, AVIO_FLAG_WRITE, NULL, NULL);
    if (res < 0) die("url_fopen");
    avformat_write_header(outContext, NULL);
    audioCodecContext = audioStream->codec;

    // resampling
    swrContext = swr_alloc();
    av_opt_set_channel_layout(swrContext, "in_channel_layout",  fileCodecContext->channel_layout, 0);
    av_opt_set_channel_layout(swrContext, "out_channel_layout", audioCodecContext->channel_layout, 0);
    av_opt_set_int(swrContext, "in_sample_rate", fileCodecContext->sample_rate, 0);
    av_opt_set_int(swrContext, "out_sample_rate", audioCodecContext->sample_rate, 0);
    av_opt_set_sample_fmt(swrContext, "in_sample_fmt", fileCodecContext->sample_fmt, 0);
    av_opt_set_sample_fmt(swrContext, "out_sample_fmt", audioCodecContext->sample_fmt, 0);
    res = swr_init(swrContext);
    if (res < 0) die("swr_init");

    AVFrame *audioFrameDecoded = av_frame_alloc();
    if (!audioFrameDecoded)
        die("Could not allocate audio frame");

    audioFrameDecoded->format = fileCodecContext->sample_fmt;
    audioFrameDecoded->channel_layout = fileCodecContext->channel_layout;
    audioFrameDecoded->channels = fileCodecContext->channels;
    audioFrameDecoded->sample_rate = fileCodecContext->sample_rate;

    AVFrame *audioFrameConverted = av_frame_alloc();
    if (!audioFrameConverted) die("Could not allocate audio frame");

    audioFrameConverted->nb_samples = audioCodecContext->frame_size;
    audioFrameConverted->format = audioCodecContext->sample_fmt;
    audioFrameConverted->channel_layout = audioCodecContext->channel_layout;
    audioFrameConverted->channels = audioCodecContext->channels;
    audioFrameConverted->sample_rate = audioCodecContext->sample_rate;

    AVPacket inPacket;
    av_init_packet(&inPacket);
    inPacket.data = NULL;
    inPacket.size = 0;

    int frameFinished = 0;

    while (av_read_frame(formatContext, &inPacket) >= 0) {
        if (inPacket.stream_index == streamId) {
            int len = avcodec_decode_audio4(fileCodecContext, audioFrameDecoded, &frameFinished, &inPacket);

            if (frameFinished) {

                // Convert

                uint8_t *convertedData=NULL;

                if (av_samples_alloc(&convertedData,
                             NULL,
                             audioCodecContext->channels,
                             audioFrameConverted->nb_samples,
                             audioCodecContext->sample_fmt, 0) < 0)
                    die("Could not allocate samples");

                int outSamples = swr_convert(swrContext, NULL, 0,
                             //&convertedData,
                             //audioFrameConverted->nb_samples,
                             (const uint8_t **)audioFrameDecoded->data,
                             audioFrameDecoded->nb_samples);
                if (outSamples < 0) die("Could not convert");

                for (;;) {
                     outSamples = swr_get_out_samples(swrContext, 0);
                     if (outSamples < audioCodecContext->frame_size * audioCodecContext->channels) break; // see comments, thanks to @dajuric for fixing this

                     outSamples = swr_convert(swrContext,
                                              &convertedData,
                                              audioFrameConverted->nb_samples, NULL, 0);

                     size_t buffer_size = av_samples_get_buffer_size(NULL,
                                    audioCodecContext->channels,
                                    audioFrameConverted->nb_samples,
                                    audioCodecContext->sample_fmt,
                                    0);
                    if (buffer_size < 0) die("Invalid buffer size");

                    if (avcodec_fill_audio_frame(audioFrameConverted,
                             audioCodecContext->channels,
                             audioCodecContext->sample_fmt,
                             convertedData,
                             buffer_size,
                             0) < 0)
                        die("Could not fill frame");

                    AVPacket outPacket;
                    av_init_packet(&outPacket);
                    outPacket.data = NULL;
                    outPacket.size = 0;

                    if (avcodec_encode_audio2(audioCodecContext, &outPacket, audioFrameConverted, &frameFinished) < 0)
                        die("Error encoding audio frame");

                    if (frameFinished) {
                        outPacket.stream_index = audioStream->index;

                        if (av_interleaved_write_frame(outContext, &outPacket) != 0)
                            die("Error while writing audio frame");

                        av_free_packet(&outPacket);
                    }
                }
            }
        }
    }

    swr_close(swrContext);
    swr_free(&swrContext);
    av_frame_free(&audioFrameConverted);
    av_frame_free(&audioFrameDecoded);
    av_free_packet(&inPacket);
    av_write_trailer(outContext);
    avio_close(outContext->pb);
    avcodec_close(fileCodecContext);
    avcodec_free_context(&fileCodecContext);
    avformat_close_input(&formatContext);

    return 0;
}

我已经更改了代码(不再更改frame_size),但仍然遇到相同的问题:http://pastebin.com/ENzeaHpU 还有其他提示吗? - david
非常好,谢谢。我会查看一下刷新情况,祝你(15小时后)获得良好的声誉 :) - david
只是一个快速的问题:如果我想在mp4中使用多个声音文件,是否明智使用每个文件一个音频流(因为pts需要增加)? - david
我不确定你的意思,你是想将两首(不同的)歌曲连接在同一个文件中吗?还是有相同内容的两个音轨(比如不同的语言)?对于第一种情况,我认为你只需要将两首歌曲连接起来,一个流就可以了。对于第二种情况,显然你需要两个流。 - Ronald S. Bultje
哦,我明白了。你想要一个混音器。基本上,你需要将两个声音样本数组以特定的权重混合在一起。有一个用于音频混合的ffmpeg过滤器,但编写自己的过滤器也很容易。我不会为此编写两个流,因为大多数播放器无法混合两个轨道,除非你使用自定义播放器。 - Ronald S. Bultje
显示剩余10条评论

2

我想在上面的代码中加入一些我发现的东西。我有一个文件陷入了无限循环。原因是该文件的采样率为48000,而代码将其更改为44100。这导致它总是有额外的outSamples。swr_convert & 无法抓取它们。所以最终我改变了add_audio_stream以匹配输入流的采样率。

        c->sample_rate = fileCodecContext->sample_rate;

另外,我需要生成wav文件作为输出。由于帧大小为0,因此我选择了一个数字,在进行了几次测试后,我选择了32。我注意到如果我选择太大的数字(例如128),会出现音频故障。

 if (audioFrameConverted->nb_samples <= 0) audioFrameConverted->nb_samples = 32; //wav files have a 0 

将跳出循环的if语句更改为在frame_size为0时检查nb_samples。

                            if ((outSamples < audioCodecContext->frame_size * audioCodecContext->channels) || audioCodecContext->frame_size==0 && (outSamples < audioFrameConverted->nb_samples * audioCodecContext->channels)) break; // see comments, thanks to @dajuric for fixing this

在测试输出到ogg文件时,我也遇到了一个小故障,时间戳数据丢失导致文件在vlc中无法正确播放。我添加了几行代码来解决这个问题。

        out_audioStream->time_base = in_audioStream->time_base; // entered before avio_open.
                        outPacket.dts = audioFrameDecoded->pkt_dts;//rest after avcodec_encode_audio2
                        outPacket.pts = audioFrameDecoded->pkt_pts;
                        av_packet_rescale_ts(&outPacket, in_audioStream->time_base, out_audioStream->time_base);

变量可能会有所不同,我把代码转换为C#。希望这能帮到某些人。


0

实际上,swr_convert 对此无效,请尝试使用 swr_convert_frame


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接