65.9K
CodeProject 正在变化。 阅读更多。
Home

在 Windows 应用商店应用中录制 WAV 音频(使用 WASAPI)

starIconstarIconstarIconstarIconstarIcon

5.00/5 (13投票s)

2013年1月20日

CPOL

6分钟阅读

viewsIcon

109025

downloadIcon

1031

开始在 Windows 应用商店应用中使用 WASAPI 录制 WAV 音频。

引言

要在 Windows 应用商店应用中录制音频,Windows 运行时提供了 MediaCapture 类,可以轻松快速地开始录制音频。但是,您只能输出 MediaEncodingProfile 中指定的可用格式,WAV 目前不在此列。

因此,要录制为 WAV,您需要另一种解决方案,并且由于您无法访问完整的 .NET 堆栈,因此您的选择有限。Microsoft 的 核心音频 SDK 中包含的 WASAPI 提供了一种解决方案。

不幸的是,要使用 WASAPI,您将从安全的托管环境抛出到非托管的 COM 部分。如果您不习惯在 .NET 中处理非托管代码,这种下降可能会很深,学习曲线也很陡峭。C# 的 dynamic 类型也无法缓解痛苦,因为使用的 COM 接口不适用于它(它们不实现 IDispatch)。

最后,将结果写入 WAV 文件还需要一些低(低于正常水平)级别的代码,如果您不习惯音频编程或其概念,这可能也是一个额外的障碍。

本文旨在帮助 WASAPI 初学者 C# 开发人员启动并运行一个基本的可用解决方案。

背景

我想尝试一个处理基本音频编辑的 Windows 应用商店应用的创意。为此,我希望使用 WAV 格式,因为它具有无损未压缩的特性以及与其他音频软件的兼容性。

由于我认为 WAV 是 Windows 上默认的未压缩音频格式,因此我期望 WinRT 对其提供开箱即用的支持。

事实并非如此,因此我转向 NAudio 作为解决方案。NAudio 将为您处理繁重的工作,与核心音频 SDK 进行通信。不幸的是,NAudio 中的 WinRT 支持正在进行中,尚未完成。它确实包含一个可用的 Windows 应用商店应用演示,用于将音频录制为 WAV,但其内置的将结果写入 WAV 文件的组件尚未在 WinRT 中提供。

我曾考虑贡献力量以添加我正在寻找的 WinRT 支持。但这要求我掌握 NAudio 库的很大一部分,才能提交一个与现有代码库及其概念很好地配合的补丁。

相反,作为起点,我尝试仅从 NAudio 中提取我需要的部分,并制作一个精简的 WinRT 兼容解决方案,但我很快意识到我有一半的内容不理解。

最后,我接受了必须直接学习处理核心音频 SDK 和编写 WAV 文件的基础知识。

在学习过程中,我发现官方渠道大多使用 C++ 作为该主题的默认语言,这给 C# 开发人员带来了额外的障碍,而不仅仅是语法差异。

因此,通过本文,我着手拼凑一个展示基础知识的解决方案,显然我不能保证最佳实践没有被违反。

必备组件

  • 假定您具备 Windows 应用商店应用开发的基础知识。
  • 假定您理解 MVVM 模式。我没有通过将从 COM 互操作到 UI 逻辑的所有内容都塞到代码隐藏中来过度简化解决方案,以避免在开发更真实的解决方案时出现意外。

Using the Code

概述

随附的 Visual Studio 2012 解决方案包含一个 Windows 应用商店应用项目,该项目演示了如何在 MVVM 设置中使用核心音频 SDK 将音频录制为 WAV。

值得注意的命名空间

  • CoreAudio 命名空间:包含与核心音频 SDK 交互的 COM 互操作逻辑
  • Services 命名空间:包含将音频录制到 WAV 文件的业务逻辑(老实说,其中不仅仅有业务逻辑,还有写入 WAV 文件的具体细节,应该将其重构出来)

要仅阅读代码而不遵循本文,请使用 ViewModels 命名空间中的 StartRecordingCommand 作为起点,并从那里遵循逻辑流程。

通过 WASAPI 捕获音频

选择用于捕获的音频设备

目标是获取一个 IAudioCaptureClient 来捕获音频。

您通过 IAudioClient 获取 IAudioCaptureClient。两者都是核心音频 SDK 的 WASAPI 的一部分。

要获取 IAudioClient,您可以通过激活音频设备来使用核心音频 SDK 的 MMDevice API。

public class WindowsMultimediaDevice
{
    [DllImport("Mmdevapi.dll", ExactSpelling = true, PreserveSig = false)]
    public static extern void ActivateAudioInterfaceAsync(
        [In, MarshalAs(UnmanagedType.LPWStr)] string deviceInterfacePath,
        [In, MarshalAs(UnmanagedType.LPStruct)] Guid riid,
        [In] IntPtr activationParams,
        [In] IActivateAudioInterfaceCompletionHandler completionHandler,
        out IActivateAudioInterfaceAsyncOperation createAsync);
}

上面的定义公开了要调用的相关方法。如果您安装了适用于 Windows 8.0 的 Windows SDK,可以在头文件 Windows Kits\8.0\Include\um\mmdeviceapi.h 中找到该定义。

Mmdevapi.dll 中的非托管代码通过 DllImport 公开,假定 Mmdevapi.dll 程序集在 Vista 及更高版本上默认可用。此外,由于非托管代码具有不同的类型,因此需要进行转换,这通过使用 MarshalAs 关键字进行封送来完成。

public void Start()
{
    _isRecording = true;
 
    var defaultAudioCaptureId = MediaDevice.GetDefaultAudioCaptureId(AudioDeviceRole.Default);
    var completionHandler = new ActivateAudioInterfaceCompletionHandler(StartCapture);
    IActivateAudioInterfaceAsyncOperation createAsync;
 
    WindowsMultimediaDevice.ActivateAudioInterfaceAsync(
        defaultAudioCaptureId, new Guid(CoreAudio.Components.WASAPI.Constants.IID_IAudioClient), 
        IntPtr.Zero, completionHandler, out createAsync);
}

使用的参数说明

  • defaultAudioCaptureId 可以通过 Windows 运行时提供的 MediaDevice 类轻松获取。
  • 但是,completionHandler 是由 MMDevice API 定义的另一种类型,请参阅 IActivateAudioInterfaceCompletionHandler 以获取详细信息。
  • 第三个参数是我们想要获取的 WASAPI COM 接口的 IID,在这种情况下是 IAudioClient。此 IID 的值可以在头文件 Windows Kits\8.0\Include\um\Audioclient.h 中找到。
  • 不需要激活参数,因此传递了与 COM 等效的 null
  • completionHandler 是将接收 IAudioClient 的回调,这是目标。
  • createAsync 在这里未使用,但为了满足方法定义而传递。

开始捕获音频

调用 ActivateAudioInterfaceAsync 后,在 ActivateAudioInterfaceCompletionHandler 回调中,使用激活的 IAudioClient 获取 IAudioCaptureClient

object audioCaptureClientInterface;
audioClient.GetService(new Guid(CoreAudio.Components.WASAPI.Constants.IID_IAudioCaptureClient), 
                       out audioCaptureClientInterface);

var audioCaptureClient = (IAudioCaptureClient)audioCaptureClientInterface;
var sleepMilliseconds = CalculateCaptureDelay(waveFormat, bufferSize);
audioClient.Start();

while (_isRecording)
{
 Task.Delay(sleepMilliseconds);
 CaptureAudioBuffer(waveFormat, bufferSize, audioCaptureClient, sleepMilliseconds);
}
audioClient.Stop();

实际的音频捕获发生在 while 循环中。老实说,具体细节完全基于 一个 MSDN C++ 示例,并借助 NAudio Windows 应用商店应用演示将其引入 C#。

据我了解,为了优化捕获过程,每次传递都会执行延迟以确保缓冲区能够跟上。敲击空缓冲区是没有意义的。

然后每次读取缓冲区,只要有可用数据(GetNextPacketSize > 0),就读取缓冲区。您正在捕获的音频设备的混合格式决定了如何解释缓冲区中的字节。

最后,通过事件通知任何订阅的客户端,并将捕获的缓冲区作为参数。

写入 WAV 文件

基本上,WAV 文件由一个指定格式详细信息的头部和实际数据组成,不同的块称为块(chunks)。

创建 WAV 文件以存储捕获的音频

获取指向要输出的文件路径的二进制写入器后,将文件准备为 WAV 文件以写入捕获的音频。
您可以在 WaveFileWriter 中找到此逻辑。

private void WriteWavRiffHeader()
{
    _binaryWriter.Write("RIFF".ToCharArray());
    _binaryWriter.Write((uint)0);               // to be updated with length of file after this point
    _binaryWriter.Write("WAVE".ToCharArray());
}

头部以主块开始,该主块指定这是一个 WAV 文件。此时文件长度未知,因此初始化为零。

private void WriteWavFormatChunkHeader(WaveFormat waveFormat)
{
    _binaryWriter.Write("fmt ".ToCharArray());

    uint samplesPerSecond = (uint)waveFormat.SampleRate;
    ushort channels = (ushort)waveFormat.Channels;
    ushort bitsPerSample = (ushort)waveFormat.BitsPerSample;
    ushort blockAlign = (ushort)(channels * (bitsPerSample / 8));
    uint averageBytesPerSec = (samplesPerSecond * blockAlign);

    _binaryWriter.Write((uint)(18 + waveFormat.ExtraSize));     // Length of header in bytes
    unchecked { _binaryWriter.Write((short)0xFFFE); }           // Format tag, 65534 
                                                                // (WAVE_FORMAT_EXTENSIBLE)
    _binaryWriter.Write(channels);                              // Number of channels
    _binaryWriter.Write(samplesPerSecond);                      // Frequency of the audio in Hz... 44100
    _binaryWriter.Write(averageBytesPerSec);                    // For estimating RAM allocation
    _binaryWriter.Write(blockAlign);                            // Sample frame size, in bytes
    _binaryWriter.Write(bitsPerSample);

    _binaryWriter.Write((short)waveFormat.ExtraSize);           // Extra param size
    _binaryWriter.Write(bitsPerSample);                         // Should be valid bits per sample
    _binaryWriter.Write((uint)3);                               // Should be channel mask
    byte[] subformat = new Guid(KsMedia.WAVEFORMATEX).ToByteArray();
    _binaryWriter.Write(subformat, 0, subformat.Length);
}

上面的下一个块使用激活的 IAudioClient 的格式指定 WAV 文件的详细信息。

private void WriteWavDataChunkHeader()
{
    // Write the data chunk
    _binaryWriter.Write("data".ToCharArray());                // Chunk id

    _dataSizePosition = _fileStream.Position;
    _binaryWriter.Write((uint)0);                             // to be updated with length of data
}

最后,实际数据之前的最后一个块指定数据的起始位置和长度,目前未知。

将捕获的音频写入 Wave 文件

写入捕获的音频很简单,接收到的字节会原样附加到文件中。

public void Write(byte[] buffer, int bytesRecorded)
{
    _fileStream.Write(buffer, 0, bytesRecorded);

    _dataChunkSize += bytesRecorded;
}

当捕获完成并写入最后一个缓冲区时,根据规范更新头部中所需的长度。

private void UpdateWavRiffHeader()
{
    _binaryWriter.Seek(4, SeekOrigin.Begin);
    _binaryWriter.Write((uint)(_binaryWriter.BaseStream.Length - 8));
}

private void UpdateDataChunkHeader()
{
    _binaryWriter.Seek((int)_dataSizePosition, SeekOrigin.Begin);
    _binaryWriter.Write((uint)_dataChunkSize);
}

参考文献

历史

  • 1.0 - 初始版本
© . All rights reserved.