在 C# 中通过 ffmpeg.exe 获取媒体文件的音频波形数据
以前这个功能我是通过 NAudio 实现的,需要把媒体文件的音频轨提取成单独的 WAV 文件才能获取到波形数据。即使借助 RAM Disk,速度也依然很慢。

忍受了一段时间后,昨天又搜了一遍 C# 下可用的音频处理类库,依然没有更好的选择。最后想到在视频转码和截图时常用的 mencoder, ffmpeg 等跨平台的命令行工具了。看了一下 ffmpeg 支持 stdout 输出,貌似可用。

最开始尝试了输出 WAV 格式的二进制流到 stdout,结果发现输出格式为 WAV 时,ffmpeg 会先在头部写一个占位的大小信息,在整个流输出结束后,再 seek 到那个位置改写大小信息 (来源)。这样当输出不是直接写入到文件,而是以 stdout 流直接输出时,这个改写操作就不能进行了,最后得到的 WAV 格式的数据流中的信息有错误,也就无法使用了。

使用 WAV 格式只是因为对它比较熟悉,毕竟是常见的最基本的直接存储型的音频文件格式了。但在翻阅 ffmpeg 文档后,发现它支持的格式非常多和灵活,可以选择直接输出整型或浮点数的波形数据,而不带额外的头信息。这正方便我用 C# 处理,最终我选择了最适用于后续处理的 Little Endian 32 位浮点数格式,也就是调用 ffmpeg.exe 时给定 "-f f32le" 参数。"-ar 44100 -ac 1" 参数则使输出固定为单声道 44100Hz 采样率的数据。

最后,完整的代码如下。Main() 方法中调用 ffmpeg.exe 将 test.mp4 的音频轨以单声道,44100Hz 采样率,32 位浮点数的形式输出到 stdout,由 ProcessStream() 对流 proc.StandardOutput.BaseStream 进行读取,最终由 ProcessBuffer() 方法获取每一个单精度浮点数。

  1. using System.Diagnostics;
  2. using System.IO;
  3.  
  4. // ...
  5.  
  6. static void Main(string[] args)
  7. {
  8.     // ...
  9.     string path = @"E:\Media\test.mp4";
  10.  
  11.     Process proc = new Process();
  12.     proc.StartInfo.FileName = @"E:\ffmpeg\ffmpeg.exe";
  13.     proc.StartInfo.Arguments = "-i \"" + path + "\" -vn -ar 44100 -ac 1 -f f32le -";
  14.     proc.StartInfo.CreateNoWindow = true;
  15.     proc.StartInfo.UseShellExecute = false;
  16.     proc.StartInfo.RedirectStandardOutput = true;
  17.     proc.StartInfo.RedirectStandardError = true;
  18.     proc.ErrorDataReceived += new DataReceivedEventHandler(proc_ErrorDataReceived);
  19.     proc.Start();
  20.     proc.BeginErrorReadLine();
  21.     ProcessStream(proc.StandardOutput.BaseStream);
  22.  
  23.     proc.WaitForExit(10000); // 10s
  24.     if (!proc.HasExited)
  25.     {
  26.         proc.Kill();
  27.         Environment.Exit(1);
  28.     }
  29.     // ...
  30. }
  31.  
  32. static void proc_ErrorDataReceived(object sender, DataReceivedEventArgs e)
  33. {
  34.     if (e.Data != null)
  35.     {
  36.         // Console.WriteLine(e.Data);
  37.         // do nothing
  38.     }
  39. }
  40.  
  41. static void ProcessStream(Stream stream)
  42. {
  43.     int didread;
  44.     int offset = 0;
  45.     byte[] buffer = new byte[sizeof(Single) * (1024 + 1)];
  46.  
  47.     int length, residual_length;
  48.  
  49.     while ((didread = stream.Read(buffer, offset, sizeof(Single) * 1024)) != 0)
  50.     {
  51.         length = offset + didread;
  52.         residual_length = length % sizeof(Single);
  53.  
  54.         if (residual_length == 0) {
  55.             ProcessBuffer(buffer, length);
  56.             offset = 0;
  57.         } else {
  58.             length -= residual_length;
  59.             ProcessBuffer(buffer, length);
  60.             Array.Copy(buffer, length, buffer, 0, residual_length);
  61.             offset = residual_length;
  62.         }
  63.     }
  64. }
  65.  
  66. static void ProcessBuffer(byte[] buffer, int length)
  67. {
  68.     int index = 0;
  69.     float sample_value;
  70.  
  71.     while (index < length)
  72.     {
  73.         sample_value = BitConverter.ToSingle(buffer, index);
  74.         index += sizeof(Single);
  75.         // to deal with sample_value
  76.     }
  77. }
  78.  
  79. // ...
当前语言: 中文 (简体) · also available in: English
3 条留言
gou9ping
2016-04-11 15:28 +0800
你好!
请问下ffmpeg 如何计算出音频的分贝呢?
skygamer
2017-12-11 18:26 +0800
Hello. I want to get waveform as stream for convert stream to image. I think your code what was i need. But i can't handle my image. I put a picturebox on my form. and want to show waveform on this picturebox.

can you help me please?
skygamer
2017-12-11 19:14 +0800
how can i deal with sample_value for draw waveform on my picturebox? It's urgent. Please.
发表留言
昵称 (必需)
邮件 (必需,不会被发布出来)
网站 (可选)
留言
可以使用类似维基标记的语法,参看指南