以前这个功能我是通过 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() 方法获取每一个单精度浮点数。
忍受了一段时间后,昨天又搜了一遍 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() 方法获取每一个单精度浮点数。
- using System.Diagnostics;
- using System.IO;
- // ...
- static void Main(string[] args)
- {
- // ...
- string path = @"E:\Media\test.mp4";
- Process proc = new Process();
- proc.StartInfo.FileName = @"E:\ffmpeg\ffmpeg.exe";
- proc.StartInfo.Arguments = "-i \"" + path + "\" -vn -ar 44100 -ac 1 -f f32le -";
- proc.StartInfo.CreateNoWindow = true;
- proc.StartInfo.UseShellExecute = false;
- proc.StartInfo.RedirectStandardOutput = true;
- proc.StartInfo.RedirectStandardError = true;
- proc.ErrorDataReceived += new DataReceivedEventHandler(proc_ErrorDataReceived);
- proc.Start();
- proc.BeginErrorReadLine();
- ProcessStream(proc.StandardOutput.BaseStream);
- proc.WaitForExit(10000); // 10s
- if (!proc.HasExited)
- {
- proc.Kill();
- Environment.Exit(1);
- }
- // ...
- }
- static void proc_ErrorDataReceived(object sender, DataReceivedEventArgs e)
- {
- if (e.Data != null)
- {
- // Console.WriteLine(e.Data);
- // do nothing
- }
- }
- static void ProcessStream(Stream stream)
- {
- int didread;
- int offset = 0;
- byte[] buffer = new byte[sizeof(Single) * (1024 + 1)];
- int length, residual_length;
- while ((didread = stream.Read(buffer, offset, sizeof(Single) * 1024)) != 0)
- {
- length = offset + didread;
- residual_length = length % sizeof(Single);
- if (residual_length == 0) {
- ProcessBuffer(buffer, length);
- offset = 0;
- } else {
- length -= residual_length;
- ProcessBuffer(buffer, length);
- Array.Copy(buffer, length, buffer, 0, residual_length);
- offset = residual_length;
- }
- }
- }
- static void ProcessBuffer(byte[] buffer, int length)
- {
- int index = 0;
- float sample_value;
- while (index < length)
- {
- sample_value = BitConverter.ToSingle(buffer, index);
- index += sizeof(Single);
- // to deal with sample_value
- }
- }
- // ...