Increase/Decrease 使用 FFmpeg 的音频音量

Increase/Decrease audio volume using FFmpeg

我目前正在使用 C# 调用来调用 FFmpeg APIs 来处理视频和音频。我有以下代码可以从视频中提取音频并将其写入文件。

while (ffmpeg.av_read_frame(formatContext, &packet) >= 0)
{
    if (packet.stream_index == streamIndex)
    {
        while (packet.size > 0)
        {
            int frameDecoded;
            int frameDecodedResult = ffmpeg.avcodec_decode_audio4(codecContext, frame, &frameDecoded, packet);

            if (frameDecoded > 0 && frameDecodedResult >= 0)
            {
                //writeAudio.WriteFrame(frame);

                packet.data += totalBytesDecoded;
                packet.size -= totalBytesDecoded;
            }
        }

        frameIndex++;
    }

    Avcodec.av_free_packet(&packet);
}

一切正常。我目前正在使用 FFmpeg.AutoGen 项目进行 API 访问。

我希望能够在将音频写入文件之前 increase/decrease 音量,但我似乎找不到命令或任何帮助。是否必须手动完成?

更新 1:

在得到一些帮助后,这是我的 class 布局:

public unsafe class FilterVolume
{
    #region Private Member Variables

    private AVFilterGraph* m_filterGraph = null;
    private AVFilterContext* m_aBufferSourceFilterContext = null;
    private AVFilterContext* m_aBufferSinkFilterContext = null;

    #endregion

    #region Private Constant Member Variables

    private const int EAGAIN = 11;

    #endregion

    public FilterVolume(AVCodecContext* codecContext, AVStream* stream, float volume)
    {
        CodecContext = codecContext;
        Stream = stream;
        Volume = volume;

        Initialise();
    }

    public AVFrame* Adjust(AVFrame* frame)
    {
        AVFrame* returnFilteredFrame = ffmpeg.av_frame_alloc();

        if (m_aBufferSourceFilterContext != null && m_aBufferSinkFilterContext != null)
        {
            int bufferSourceAddFrameResult = ffmpeg.av_buffersrc_add_frame(m_aBufferSourceFilterContext, frame);
            if (bufferSourceAddFrameResult < 0)
            {
            }

            int bufferSinkGetFrameResult = ffmpeg.av_buffersink_get_frame(m_aBufferSinkFilterContext, returnFilteredFrame);
            if (bufferSinkGetFrameResult < 0 && bufferSinkGetFrameResult != -EAGAIN)
            {
            }
        }

        return returnFilteredFrame;
    }

    public void Dispose()
    {
        Cleanup(m_filterGraph);
    }

    #region Private Properties

    private AVCodecContext* CodecContext { get; set; }
    private AVStream* Stream { get; set; }
    private float Volume { get; set; }

    #endregion

    #region Private Setup Helper Functions

    private void Initialise()
    {
        m_filterGraph = GetAllocatedFilterGraph();

        string aBufferFilterArguments = string.Format("sample_fmt={0}:channel_layout={1}:sample_rate={2}:time_base={3}/{4}",
            (int)CodecContext->sample_fmt,
            CodecContext->channel_layout,
            CodecContext->sample_rate,
            Stream->time_base.num,
            Stream->time_base.den);

        AVFilterContext* aBufferSourceFilterContext = CreateFilter("abuffer", m_filterGraph, aBufferFilterArguments);
        AVFilterContext* volumeFilterContext = CreateFilter("volume", m_filterGraph, string.Format("volume={0}", Volume));
        AVFilterContext* aBufferSinkFilterContext = CreateFilter("abuffersink", m_filterGraph);

        LinkFilter(aBufferSourceFilterContext, volumeFilterContext);
        LinkFilter(volumeFilterContext, aBufferSinkFilterContext);

        SetFilterGraphConfiguration(m_filterGraph, null);

        m_aBufferSourceFilterContext = aBufferSourceFilterContext;
        m_aBufferSinkFilterContext = aBufferSinkFilterContext;
    }

    #endregion

    #region Private Cleanup Helper Functions

    private static void Cleanup(AVFilterGraph* filterGraph)
    {
        if (filterGraph != null)
        {
            ffmpeg.avfilter_graph_free(&filterGraph);
        }
    }

    #endregion

    #region Provate Helpers

    private AVFilterGraph* GetAllocatedFilterGraph()
    {
        AVFilterGraph* filterGraph = ffmpeg.avfilter_graph_alloc();
        if (filterGraph == null)
        {
        }

        return filterGraph;
    }

    private AVFilter* GetFilterByName(string name)
    {
        AVFilter* filter = ffmpeg.avfilter_get_by_name(name);
        if (filter == null)
        {
        }

        return filter;
    }

    private void SetFilterGraphConfiguration(AVFilterGraph* filterGraph, void* logContext)
    {
        int filterGraphConfigResult = ffmpeg.avfilter_graph_config(filterGraph, logContext);
        if (filterGraphConfigResult < 0)
        {
        }
    }

    private AVFilterContext* CreateFilter(string filterName, AVFilterGraph* filterGraph, string filterArguments = null)
    {
        AVFilter* filter = GetFilterByName(filterName);
        AVFilterContext* filterContext;

        int aBufferFilterCreateResult = ffmpeg.avfilter_graph_create_filter(&filterContext, filter, filterName, filterArguments, null, filterGraph);
        if (aBufferFilterCreateResult < 0)
        {
        }

        return filterContext;
    }

    private void LinkFilter(AVFilterContext* source, AVFilterContext* destination)
    {
        int filterLinkResult = ffmpeg.avfilter_link(source, 0, destination, 0);
        if (filterLinkResult < 0)
        {
        }
    }

    #endregion
}

帧解码后调用Adjust()函数。当调用 av_buffersrc_add_frame() 时,我目前收到 -22 错误。这表示参数无效,但调试后,我看不到任何导致此问题的原因。

代码是这样调用的:

while (ffmpeg.av_read_frame(formatContext, &packet) >= 0)
{
    if (packet.stream_index == streamIndex)
    {
        while (packet.size > 0)
        {
            int frameDecoded;
            int frameDecodedResult = ffmpeg.avcodec_decode_audio4(codecContext, frame, &frameDecoded, packet);

            if (frameDecoded > 0 && frameDecodedResult >= 0)
            {
                AVFrame* filteredFrame = m_filterVolume.Adjust(frame);

                //writeAudio.WriteFrame(filteredFrame);

                packet.data += totalBytesDecoded;
                packet.size -= totalBytesDecoded;
            }
        }

        frameIndex++;
    }

    Avcodec.av_free_packet(&packet);
}

更新二:

破解了。过滤器参数字符串中的 "channel_layout" 选项应该是十六进制的。字符串格式应如下所示:

string aBufferFilterArguments = string.Format("sample_fmt={0}:channel_layout=0x{1}:sample_rate={2}:time_base={3}/{4}",
    (int)CodecContext->sample_fmt,
    CodecContext->channel_layout,
    CodecContext->sample_rate,
    Stream->time_base.num,
    Stream->time_base.den);

我不知道你用的是什么API,但是ffmpeg有一个命令可以增加或减少音频:

减半:

ffmpeg -i input.wav -af "volume=0.5" output.wav

增加 50%:

ffmpeg -i input.wav -af "volume=1.5" output.wav

或以 dB 为单位:

ffmpeg -i input.wav -af "volume=10dB" output.wav

希望对你有帮助

您需要做的是构建一个过滤图并通过该图处理音频流。在您的情况下,图表只是输入 ("abuffer") -> 音量 -> 输出 ("abuffersink")。 这是一个示例控制台应用程序,用于演示这一点。它大致基于 ffmpeg 样本 filtering_audio, filter_audio and remuxing.

你可以这样使用它:

ChangeVolume.exe http://www.quirksmode.org/html5/videos/big_buck_bunny.mp4 bunny_half.mp4 0.5

这是代码:

class Program
{
    static unsafe void Main(string[] args)
    {
        Console.WriteLine(@"Current directory: " + Environment.CurrentDirectory);
        Console.WriteLine(@"Running in {0}-bit mode.", Environment.Is64BitProcess ? @"64" : @"32");

        // adapt this to your context
        var ffmpegPath = string.Format(@"../../../FFmpeg/bin/{0}", Environment.Is64BitProcess ? @"x64" : @"x86");
        InteropHelper.SetDllDirectory(ffmpegPath);

        int ret, i;
        if (args.Length < 3)
        {
            Console.WriteLine("usage: ChangeVolume input output <volume ratio>");
            return;
        }

        string in_filename = args[0];
        string out_filename = args[1];
        double ratio = double.Parse(args[2]); 
        ffmpeg.av_register_all();
        ffmpeg.avfilter_register_all();

        // open input file
        AVFormatContext* ifmt_ctx = null;
        InteropHelper.Check(ffmpeg.avformat_open_input(&ifmt_ctx, in_filename, null, null));

        // dump input
        ffmpeg.av_dump_format(ifmt_ctx, 0, in_filename, 0);

        // get streams info to determine audio stream index
        InteropHelper.Check(ffmpeg.avformat_find_stream_info(ifmt_ctx, null));

        // determine input decoder
        AVCodec* dec;
        int audio_stream_index = ffmpeg.av_find_best_stream(ifmt_ctx, AVMediaType.AVMEDIA_TYPE_AUDIO, -1, -1, &dec, 0);
        AVCodecContext* dec_ctx = ifmt_ctx->streams[audio_stream_index]->codec;

        // open input decoder
        InteropHelper.Check(ffmpeg.avcodec_open2(dec_ctx, dec, null));

        // build a filter graph
        AVFilterContext* buffersrc_ctx;
        AVFilterContext* buffersink_ctx;
        AVFilterGraph* filter_graph = init_filter_graph(ifmt_ctx, dec_ctx, audio_stream_index, &buffersrc_ctx, &buffersink_ctx, ratio);

        // prepare output
        AVFormatContext* ofmt_ctx = null;
        InteropHelper.Check(ffmpeg.avformat_alloc_output_context2(&ofmt_ctx, null, null, out_filename));
        InteropHelper.Check(ofmt_ctx);

        // create output streams
        AVCodecContext* enc_ctx = null;
        ofmt_ctx->oformat->flags |= InteropHelper.AVFMT_NOTIMESTAMPS;
        for (i = 0; i < ifmt_ctx->nb_streams; i++)
        {
            AVStream* in_stream = ifmt_ctx->streams[i];
            if (in_stream->codec->codec_type == AVMediaType.AVMEDIA_TYPE_DATA) // skip these
                continue;

            AVStream* out_stream = ffmpeg.avformat_new_stream(ofmt_ctx, in_stream->codec->codec);
            InteropHelper.Check(out_stream);

            InteropHelper.Check(ffmpeg.avcodec_copy_context(out_stream->codec, in_stream->codec));

            out_stream->codec->codec_tag = 0;
            if ((ofmt_ctx->oformat->flags & InteropHelper.AVFMT_GLOBALHEADER) != 0)
            {
                out_stream->codec->flags |= InteropHelper.AV_CODEC_FLAG_GLOBAL_HEADER;
            }

            if (i == audio_stream_index)
            {
                // create audio encoder from audio decoder
                AVCodec* enc = ffmpeg.avcodec_find_encoder(dec_ctx->codec_id);
                InteropHelper.Check(enc);

                enc_ctx = ffmpeg.avcodec_alloc_context3(enc);
                InteropHelper.Check(enc_ctx);

                enc_ctx->sample_rate = dec_ctx->sample_rate;
                enc_ctx->channel_layout = dec_ctx->channel_layout;
                enc_ctx->channels = ffmpeg.av_get_channel_layout_nb_channels(enc_ctx->channel_layout);
                enc_ctx->sample_fmt = enc->sample_fmts[0];
                enc_ctx->time_base.num = 1;
                enc_ctx->time_base.den = enc_ctx->sample_rate;
                InteropHelper.Check(ffmpeg.avcodec_open2(enc_ctx, enc, null));
            }
        }

        // dump output
        ffmpeg.av_dump_format(ofmt_ctx, 0, out_filename, 1);

        if ((ofmt_ctx->oformat->flags & InteropHelper.AVFMT_NOFILE) == 0)
        {
            // open output file
            InteropHelper.Check(ffmpeg.avio_open(&ofmt_ctx->pb, out_filename, InteropHelper.AVIO_FLAG_WRITE));
        }

        // write output file header
        InteropHelper.Check(ffmpeg.avformat_write_header(ofmt_ctx, null));

        // read all packets and process
        AVFrame* frame = ffmpeg.av_frame_alloc();
        AVFrame* filt_frame = ffmpeg.av_frame_alloc();
        while (true)
        {
            AVStream* in_stream;
            AVStream* out_stream;
            AVPacket pkt;
            ret = ffmpeg.av_read_frame(ifmt_ctx, &pkt);
            if (ret < 0)
                break;

            in_stream = ifmt_ctx->streams[pkt.stream_index];
            if (in_stream->codec->codec_type == AVMediaType.AVMEDIA_TYPE_DATA)
                continue;

            // audio stream? we need to pass it through our filter graph
            if (pkt.stream_index == audio_stream_index)
            {
                // decode audio (packet -> frame)
                int got_frame = 0;
                InteropHelper.Check(ffmpeg.avcodec_decode_audio4(dec_ctx, frame, &got_frame, &pkt));

                if (got_frame > 0)
                {
                    // add the frame into the filter graph
                    InteropHelper.Check(ffmpeg.av_buffersrc_add_frame(buffersrc_ctx, frame));
                    while (true)
                    {
                        // get the frame out from the filter graph
                        ret = ffmpeg.av_buffersink_get_frame(buffersink_ctx, filt_frame);
                        const int EAGAIN = 11;
                        if (ret == -EAGAIN)
                            break;

                        InteropHelper.Check(ret);

                        // encode audio (frame -> packet)
                        AVPacket enc_pkt = new AVPacket();
                        int got_packet = 0;
                        InteropHelper.Check(ffmpeg.avcodec_encode_audio2(enc_ctx, &enc_pkt, filt_frame, &got_packet));
                        enc_pkt.stream_index = pkt.stream_index;
                        InteropHelper.Check(ffmpeg.av_interleaved_write_frame(ofmt_ctx, &enc_pkt));
                        ffmpeg.av_frame_unref(filt_frame);
                    }
                }
            }
            else
            {
                // write other (video) streams
                out_stream = ofmt_ctx->streams[pkt.stream_index];
                pkt.pts = ffmpeg.av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base, AVRounding.AV_ROUND_NEAR_INF | AVRounding.AV_ROUND_PASS_MINMAX);
                pkt.dts = ffmpeg.av_rescale_q_rnd(pkt.dts, in_stream->time_base, out_stream->time_base, AVRounding.AV_ROUND_NEAR_INF | AVRounding.AV_ROUND_PASS_MINMAX);
                pkt.duration = ffmpeg.av_rescale_q(pkt.duration, in_stream->time_base, out_stream->time_base);
                pkt.pos = -1;
                InteropHelper.Check(ffmpeg.av_interleaved_write_frame(ofmt_ctx, &pkt));
            }
            ffmpeg.av_packet_unref(&pkt);
        }

        // write trailer, close file
        ffmpeg.av_write_trailer(ofmt_ctx);
        ffmpeg.avformat_close_input(&ifmt_ctx);
        if ((ofmt_ctx->oformat->flags & InteropHelper.AVFMT_NOFILE) == 0)
        {
            ffmpeg.avio_closep(&ofmt_ctx->pb);
        }

        ffmpeg.avformat_free_context(ofmt_ctx);

        ffmpeg.av_frame_free(&filt_frame);
        ffmpeg.av_frame_free(&frame);

        ffmpeg.avfilter_graph_free(&filter_graph);
        return;
    }

    static unsafe AVFilterGraph* init_filter_graph(AVFormatContext* format, AVCodecContext* codec, int audio_stream_index, AVFilterContext** buffersrc_ctx, AVFilterContext** buffersink_ctx, double volumeRatio)
    {
        // create graph
        var filter_graph = ffmpeg.avfilter_graph_alloc();
        InteropHelper.Check(filter_graph);

        // add input filter
        var abuffersrc = ffmpeg.avfilter_get_by_name("abuffer");
        if (abuffersrc == null) InteropHelper.CheckTag("\x00F8FIL");
        string args = string.Format("sample_fmt={0}:channel_layout={1}:sample_rate={2}:time_base={3}/{4}",
            (int)codec->sample_fmt,
            codec->channel_layout,
            codec->sample_rate,
            format->streams[audio_stream_index]->time_base.num,
            format->streams[audio_stream_index]->time_base.den);
        InteropHelper.Check(ffmpeg.avfilter_graph_create_filter(buffersrc_ctx, abuffersrc, "IN", args, null, filter_graph));

        // add volume filter
        var volume = ffmpeg.avfilter_get_by_name("volume");
        if (volume == null) InteropHelper.CheckTag("\x00F8FIL");
        AVFilterContext* volume_ctx;
        InteropHelper.Check(ffmpeg.avfilter_graph_create_filter(&volume_ctx, volume, "VOL", "volume=" + volumeRatio.ToString(CultureInfo.InvariantCulture), null, filter_graph));

        // add output filter
        var abuffersink = ffmpeg.avfilter_get_by_name("abuffersink");
        if (abuffersink == null) InteropHelper.CheckTag("\x00F8FIL");
        InteropHelper.Check(ffmpeg.avfilter_graph_create_filter(buffersink_ctx, abuffersink, "OUT", "", null, filter_graph));

        // connect input -> volume -> output
        InteropHelper.Check(ffmpeg.avfilter_link(*buffersrc_ctx, 0, volume_ctx, 0));
        InteropHelper.Check(ffmpeg.avfilter_link(volume_ctx, 0, *buffersink_ctx, 0));
        InteropHelper.Check(ffmpeg.avfilter_graph_config(filter_graph, null));
        return filter_graph;
    }
}

它使用派生自 AutoGen 的实用程序 InteropHelper class:

public class InteropHelper
{
    [DllImport("kernel32", SetLastError = true)]
    public static extern bool SetDllDirectory(string lpPathName);

    public static readonly int AVERROR_EOF = -GetTag("EOF ");
    public static readonly int AVERROR_UNKNOWN = -GetTag("UNKN");
    public static readonly int AVFMT_GLOBALHEADER = 0x0040;
    public static readonly int AVFMT_NOFILE = 0x0001;
    public static readonly int AVIO_FLAG_WRITE = 2;
    public static readonly int AV_CODEC_FLAG_GLOBAL_HEADER = (1 << 22);
    public static readonly int AV_ROUND_ZERO = 0;
    public static readonly int AV_ROUND_INF = 1;
    public static readonly int AV_ROUND_DOWN = 2;
    public static readonly int AV_ROUND_UP = 3;
    public static readonly int AV_ROUND_PASS_MINMAX = 8192;
    public static readonly int AV_ROUND_NEAR_INF = 5;
    public static readonly int AVFMT_NOTIMESTAMPS = 0x0080;

    public static unsafe void Check(void* ptr)
    {
        if (ptr != null)
            return;

        const int ENOMEM = 12;
        Check(-ENOMEM);
    }

    public static unsafe void Check(IntPtr ptr)
    {
        if (ptr != IntPtr.Zero)
            return;

        Check((void*)null);
    }

    // example: "\x00F8FIL" is "Filter not found" (check libavutil/error.h)
    public static void CheckTag(string tag)
    {
        Check(-GetTag(tag));
    }

    public static int GetTag(string tag)
    {
        var bytes = new byte[4];
        for (int i = 0; i < 4; i++)
        {
            bytes[i] = (byte)tag[i];
        }
        return BitConverter.ToInt32(bytes, 0);
    }

    public static void Check(int res)
    {
        if (res >= 0)
            return;

        string err = "ffmpeg error " + res;
        string text = GetErrorText(res);
        if (!string.IsNullOrWhiteSpace(text))
        {
            err += ": " + text;
        }
        throw new Exception(err);
    }

    public static string GetErrorText(int res)
    {
        IntPtr err = Marshal.AllocHGlobal(256);
        try
        {
            ffmpeg.av_strerror(res, err, 256);
            return Marshal.PtrToStringAnsi(err);
        }
        finally
        {
            Marshal.FreeHGlobal(err);
        }
    }
}