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

在 Windows 8 (VB) 中录制和播放 PCM 音频

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (9投票s)

2012年9月17日

公共领域

9分钟阅读

viewsIcon

128939

downloadIcon

2774

如何在 .NET 中使用 Windows 8 录制和播放 PCM 音频。

引言

此代码在 Windows 8 上以 PCM 格式录制和播放原始音频。代码是用 VB 编写的,但相同技术也适用于 C#。

它使用了 IAudioClientIAudioRenderClient IAudioCaptureClient,它们是 Windows 音频会话 API (WASAPI) 的一部分。

Windows 提供了几种不同的 API 系列用于录制和播放音频。

  • MediaElement - 所有 UI 框架都有一个 MediaElement 控件 - Windows 8 的 XAML、WPF、Silverlight、WinForms。它们允许你播放音频和视频。但它们没有录制接口,也不允许你发送自己的原始音频数据。(你能做的最好的就是创建一个 WAV 文件并告诉它们播放它)。
  • waveOut 和 waveIn - 这些遗留 API 曾是 .NET 中录制和播放原始音频最简单易用的库。不幸的是,它们不允许在 Windows 应用商店应用程序中使用。
  • WASAPI (Windows 音频会话 API) - WASAPI 于 Vista 引入,这是一个 C++/COM API,是当前推荐的录制和播放音频的新方法。
  • xAudio2 - 这是一个低级 C++/COM 音频 API,面向游戏开发者,并允许在 Windows 应用商店应用程序中使用。它是 DirectSound 的后继者。

NAudio。作为 .NET 开发者,如果你需要音频功能,你的首选通常应该是 http://naudio.codeplex.com/。NAudio 是一个高质量的库,可以通过 NuGet 获取,并根据 MS-Pl 许可,用于为 VB 和 C# 应用程序添加音频功能。它支持 waveOut/waveIn、WASAPI 和 DirectSound。

Sharpdx。这是一个 DirectX 的托管包装器,位于 http://www.sharpdx.org/。它封装了 xAudio2 等功能。它也曾成功地用于 Windows 应用商店的提交。所以它也是一个选择。

我写这篇文章是因为我当时不知道 Sharpdx,而且(截至 2012 年 9 月)NAudio 在 Windows 8 应用商店应用程序中无法工作。这是因为它包含了对不允许的遗留 API 的调用。此外,Windows 8 应用商店应用程序需要一种与 NAudio 使用的传统入口点不同的“入口点”来访问 WASAPI。

微软提供了一些关于使用 WASAPI 进行“捕获流”和“渲染流”的出色 C++ 教程。本文基本上是这些示例的 .NET 和 WinRT 等效版本。

使用代码

COM 互操作。WASAPI 是一个完全基于 COM 的 API,需要 P/Invoke 互操作包装器才能在 .NET 中工作。此外,在音频应用程序中,你需要处理大量数据,并且及时释放资源非常重要。VB 和 C# 使用 IDispose 机制来处理这个问题,并依赖 .NET 的垃圾回收来处理其他所有事情。C++/COM 使用 IUnknown.Release 和引用计数。在两者之间架起桥梁很困难。

如果你只想阅读关于 WASAPI 的内容,可以跳过本节。但我建议你不要这样做,因为你需要这些技能才能正确使用 WASAPI。

我推荐阅读 MSDN 的这篇文章“提高互操作性能”,C++ Team 的这篇博客文章“混合确定性和非确定性清理”,以及 Lim Bio Liong 的这篇博客“RCW 内部引用计数”。

<ComImport, Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")>
Class MMDeviceEnumerator
End Class

<ComImport, Guid("A95664D2-9614-4F35-A746-DE8DB63617E6")>
<InterfaceType(ComInterfaceType.InterfaceIsIUnknown)>
Interface IMMDeviceEnumerator
    Sub GetDefaultAudioEndpoint(dflow As EDataFlow, role As ERole, ByRef ppDevice As IMMDevice)
End Interface

10: Dim e As New MMDeviceEnumerator
20: Dim i = CType(e, IMMDeviceEnumerator)
...
30: Marshal.FinalReleaseComObject(i)
40: i = Nothing : e = Nothing

下面是对上述代码的解释。我们将关注 COM 对象,它维护一个引用计数,以及它对应的 .NET Runtime Callable Wrapper (RCW),它有自己的内部引用计数。RCW 与 IUnknown IntPtr 之间存在一对一的关系。当 IUnknown IntPtr 第一次进入托管代码时(例如,通过分配新的 COM 对象或 Marshal.ObjectFromIUnknown),RCW 被创建,其内部引用计数设置为 1,并且它只调用一次 IUnknown.AddRef。之后,当同一个 IUnknown IntPtr 再次进入托管代码时,RCW 的内部引用计数会递增,但不会调用 IUnknown.AddRef。RCW 的内部引用计数通过正常的垃圾回收而递减;你也可以手动使用 Marshal.ReleaseComObject 来递减它,或者使用 Marshal.FinalReleaseComObject 直接将其强制为 0。当 RCW 的内部引用计数降至 0 时,它只调用一次 IUnknown.Release。之后对其进行的任何方法调用或转换都会失败,并显示消息:“COM 对象已与其底层 RCW 分离,无法使用”。

第 10 行之后:我们有一个引用计数为“1”的 COM 对象,而“e”是对其 RCW 的引用,该 RCW 的内部引用计数为“1”。

第 20 行之后:同一个 COM 对象仍然具有引用计数“1”,而“i”是对之前相同 RCW 的引用,该 RCW 的内部引用计数仍然是“1”。

第 30 行之后:RCW 的引用计数降至“0”,并且对 COM 对象调用了 IUnknown.Release,该对象现在具有引用计数“0”。对“i”和“e”的任何进一步方法调用或转换都会失败。

建议实践:创建一个实现 IDisposable 的包装器类,其中包含一个用于 COM 对象(或更确切地说,其 RCW)的私有字段。绝不要让你的客户端直接访问 COM 对象。如果需要,包装器类中的局部变量引用同一个 RCW 是可以的。在 IDisposable.Dispose 中,调用 Marshal.FinalReleaseComObject,将字段设置为 Nothing,并确保你的包装器永远不再访问该字段的方法或字段。

枚举音频设备并获取 IAudioClient

.NET 4.5:这是用于枚举音频设备并获取其中一个设备的 IAudioClient 的 .NET 4.5 代码。

Dim pEnum = CType(New MMDeviceEnumerator, IMMDeviceEnumerator)

' Enumerating devices...
Dim pDevices As IMMDeviceCollection = Nothing : _
    pEnum.EnumAudioEndpoints(EDataFlow.eAll, _
    DeviceStateFlags.DEVICE_STATE_ACTIVE, pDevices)
Dim pcDevices = 0 : pDevices.GetCount(pcDevices)
For i = 0 To pcDevices - 1
    Dim pDevice As IMMDevice = Nothing : pDevices.Item(i, pDevice)
    Dim pProps As IPropertyStore = Nothing : pDevice.OpenPropertyStore(StgmMode.STGM_READ, pProps)
    Dim varName As PROPVARIANT = Nothing : pProps.GetValue(PKEY_Device_FriendlyName, varName)
    ' CStr(varName.Value) - is the name of the device
    PropVariantClear(varName)
    Runtime.InteropServices.Marshal.FinalReleaseComObject(pProps) : pProps = Nothing
    Runtime.InteropServices.Marshal.FinalReleaseComObject(pDevice)
Next
Runtime.InteropServices.Marshal.FinalReleaseComObject(pDevices) : pDevices = Nothing

' Or, instead of enumerating, we can get the default device directly.
' Use EDataFlow.eRender for speakers, and .eCapture for microphone.
Dim pDevice As IMMDevice = Nothing : pEnum.GetDefaultAudioEndpoint(EDataFlow.eRender,  ERole.eConsole, pDevice)

' Once we have an IMMDevice, this is how we get the IAudioClient
Dim pAudioClient As IAudioClient2 = Nothing : pDeviceR.Activate(

                 IID_IAudioClient2, CLSCTX.CLSCTX_ALL, Nothing, pAudioClient)
...
Runtime.InteropServices.Marshal.FinalReleaseComObject(pAudioClient) : pAudioClient = Nothing
Runtime.InteropServices.Marshal.FinalReleaseComObject(pEnum) : pEnum = Nothing

Windows 8:上面的 COM 对象不允许在 Windows 应用商店应用程序中使用,因此我们必须使用不同的技术。让我们从枚举音频设备并获取默认设备的代码开始。

' What is the default device for recording audio?
Dim defaultDeviceIdR = Windows.Media.Devices.MediaDevice.GetDefaultAudioCaptureId(
           Windows.Media.Devices.AudioDeviceRole.Default)

' Let's enumerate all the recording devices...
Dim audioSelectorR = Windows.Media.Devices.MediaDevice.GetAudioCaptureSelector()
Dim devicesR = Await Windows.Devices.Enumeration.DeviceInformation.FindAllAsync(
      audioSelectorR, {PKEY_AudioEndpoint_Supports_EventDriven_Mode.ToString()})

For Each device In devicesR
    ' use device.Id, device.Name, ...
Next


' What is the default device for playing audio?
Dim defaultDeviceIdP = Windows.Media.Devices.MediaDevice.GetDefaultAudioRenderId(
        Windows.Media.Devices.AudioDeviceRole.Default)

' Enumerate all the playback devices in the same way...
Dim audioSelectorP = Windows.Media.Devices.MediaDevice.GetAudioRenderSelector()
Dim devicesP = Await Windows.Devices.Enumeration.DeviceInformation.FindAllAsync(
    audioSelectorP, {PKEY_AudioEndpoint_Supports_EventDriven_Mode.ToString()})  

麦克风权限。在 Windows 8 中,我们需要获取一个 IAudioClient 来用于选择的录制/播放设备。这并不直接。如果你在应用商店应用程序中使用这些 API,并且想要为录制设备初始化一个 IAudioClient,那么你的应用程序第一次尝试这样做时,会弹出一个提示,询问用户是否允许应用程序录制音频。Windows 8 出于隐私原因这样做,以防止应用程序偷偷窃听用户。Windows 会记住用户的答案,用户将不再看到该提示(除非卸载+重装应用程序)。如果用户改变主意,他们可以启动你的应用程序,然后通过“魅力”菜单 > “设备” > “权限”来更改设置。微软为此撰写了更详细的“访问个人数据的设备指南”,其中包括向用户展示此信息的 UI 指南。顺便说一下,对于桌面应用程序,权限始终是隐含授予的,并且播放音频根本不需要任何权限。

Dim icbh As New ActivateAudioInterfaceCompletionHandler(
    Sub(pAudioClient As IAudioClient2)
        Dim wfx As New WAVEFORMATEX With {.wFormatTag = 1, .nChannels = 2,
                   .nSamplesPerSec = 44100, .wBitsPerSample = 16, .nBlockAlign = 4,
                   .nAvgBytesPerSec = 44100 * 4, .cbSize = 0}
        pAudioClient.Initialize(
                     AUDCLNT_SHAREMODE.AUDCLNT_SHAREMODE_SHARED,
                     AUDCLNT_FLAGS.AUDCLNT_STREAMFLAGS_EVENTCALLBACK Or
                     AUDCLNT_FLAGS.AUDCLNT_STREAMFLAGS_NOPERSIST,
                     10000000, 0, wfx, Nothing)
    End Sub)

Dim activationOperation As IActivateAudioInterfaceAsyncOperation = Nothing
ActivateAudioInterfaceAsync(defaultDeviceIdR, IID_IAudioClient2, Nothing,
                            icbh, activationOperation)

Try
    Dim pAudioClient = Await icbh 

    ...

    Runtime.InteropServices.Marshal.FinalReleaseComObject(pAudioClient)
    pAudioClient = Nothing 
Catch ex As UnauthorizedAccessException When ex.HResult = -2147024891
    ' OOPS! Can't record. Charms > Settings > Permissions to grant microphone permission
Finally 
    Runtime.InteropServices.Marshal.FinalReleaseComObject(activationOperation)
    activationOperation = Nothing 
End Try   

它的工作方式是,你构建自己的对象,在本例中是 icbhR,它实现了 IActivateAudioInterfaceCompletionHandlerIAgileObject。然后你调用 ActivateAudioInterfaceAsync,并将此对象传递进去。过了一会儿,在另一个线程上,你的对象的 IActivateAudioInterfaceCompletionHandler.ActivateCompleted 方法将被调用。在回调中,你获取 IAudioClient 接口,然后用你选择的 PCM 格式在其上调用 IAudioClient.Initialize()。如果 Windows 需要弹出权限提示,那么对 Initialize() 的调用将在此提示显示期间阻塞。之后,对 Initialize() 的调用将成功(如果已授予权限),或因 UnauthorizedAccessException 而失败(如果未授予权限)。你必须在回调中调用 Initialize,否则你的应用程序将在 Initialize 调用上无限期阻塞。

MSDN 关于 ActivateAudioInterfaceAsync 的文档说 ActivateAudioInterfaceAsync 第一次调用时可能会显示一个同意提示。这是不正确的。是 Initialize() 可能会显示同意提示;ActivateAudioInterfaceAsync 绝不会显示。他们还说“在 Windows 8 中,首次使用 IAudioClient 访问音频设备应在 STA 线程上进行。来自 MTA 线程的调用可能会导致未定义的行为。”这也是不正确的。首次使用 IAudioClient.Initialize 必须在你的 IActivateAudioInterfaceCompletionHandler.ActivateCompleted 处理程序内部进行,该处理程序将由 ActivateAudioInterfaceAsync 在后台线程上调用。

上面的代码要求你自己实现 icbh 对象。这是我的实现。我让它实现了“awaiter 模式”并带有一个名为 GetAwaiter 的方法:这使我们能够像上面的代码一样简单地 Await icbh。这段代码展示了 TaskCompletionSource 的典型用法,将一个使用回调的 API 转换为一个你可以 await 的更友好的 API。

Class ActivateAudioInterfaceCompletionHandler
    Implements IActivateAudioInterfaceCompletionHandler, IAgileObject

    Private InitializeAction As Action(Of IAudioClient2)
    Private tcs As New TaskCompletionSource(Of IAudioClient2)

    Sub New(InitializeAction As Action(Of IAudioClient2))
        Me.InitializeAction = InitializeAction
    End Sub

    Public Sub ActivateCompleted(activateOperation As IActivateAudioInterfaceAsyncOperation) _
               Implements IActivateAudioInterfaceCompletionHandler.ActivateCompleted
        ' First get the activation results, and see if anything bad happened then
        Dim hr As Integer = 0, unk As Object = Nothing : activateOperation.GetActivateResult(hr, unk)
        If hr <> 0 Then
            tcs.TrySetException(Runtime.InteropServices.Marshal.GetExceptionForHR(hr, New IntPtr(-1)))
            Return
        End If

        Dim pAudioClient = CType(unk, IAudioClient2)

        ' Next try to call the client's (synchronous, blocking) initialization method.
        Try
            InitializeAction(pAudioClient)
            tcs.SetResult(pAudioClient)
        Catch ex As Exception
            tcs.TrySetException(ex)
        End Try
    End Sub

    Public Function GetAwaiter() As Runtime.CompilerServices.TaskAwaiter(Of IAudioClient2)
        Return tcs.Task.GetAwaiter()
    End Function
End Class

MSDN 关于 IActivateAudioInterfaceCompletionHandler 的文档说该对象必须是“一个 agile 对象(聚合了一个 free-threaded marshaler)”。这是不正确的。该对象的 IMarshal 接口根本从未被检索过。所有要求的是它实现 IAgileObject

选择音频格式

在上面的代码中,作为 IAudioClient.Initialize 方法的参数,我直接选择了 CD 质量的音频(立体声、每样本 16 位、44100Hz)。你只能选择设备原生支持的格式,而且许多设备(包括 Surface)甚至不支持这种格式……

还有其他一些选择音频格式的方法。我通常不喜欢它们,因为它们返回的 WAVEFORMATEX 结构末尾带有一堆额外的操作系统特定数据。这意味着如果你想将给定的 IntPtr 传递给 Initialize(),你必须保留它,并在最后对其调用 Marshal.FreeCoTaskMem。(或者:NAudio 的做法更优雅:它定义了自己的自定义 marshaler,能够将额外的数据封送进出)。

' Easiest way to pick an audio format for testing:
Dim wfx As New WAVEFORMATEX With {.wFormatTag = 1, .nChannels = 2, .nSamplesPerSec = 44100,
                                  .wBitsPerSample = 16, .nBlockAlign = 4,
                                  .nAvgBytesPerSec = 44100 * 4, .cbSize = 0}


' Another way: you could get the preferred audio format of the device
Dim pwfx_default As IntPtr = Nothing : pAudioClient.GetMixFormat(pwfx_default) 
Dim wfx_default = CType(Runtime.InteropServices.Marshal.PtrToStructure(pwfx_default, GetType(WAVEFORMATEX)), WAVEFORMATEX)
If pwfx_default <> Nothing Then Runtime.InteropServices.Marshal.FreeCoTaskMem(pwfx_default) : pwfx_default = Nothing


' Or you could pass in your own requested format, and if it's not supported "as is",
' then it'll suggest a closest match. (If it's okay as-is, then pwfx_suggested = Nothing)
Dim pwfx_suggested As IntPtr = Nothing : pAudioClient.IsFormatSupported(AUDCLNT_SHAREMODE.AUDCLNT_SHAREMODE_SHARED, wfx, pwfx_suggested)
If pwfx_suggested <> Nothing Then
    Dim wfx_suggested = CType(Runtime.InteropServices.Marshal.PtrToStructure(pwfx_suggested, GetType(WAVEFORMATEX)), WAVEFORMATEX)
    Runtime.InteropServices.Marshal.FreeCoTaskMem(pwfx_suggested)
End If

录制音频

这是录制音频的代码。在这种情况下,我已经分配了一个足够容纳 10 秒音频的缓冲区“buf”,我只是将其复制进去。你可能更想使用较小的缓冲区并重复使用它们。

Dim hEvent = CreateEventEx(Nothing, Nothing, 0, EventAccess.EVENT_ALL_ACCESS)
pAudioClient.SetEventHandle(hEvent)
Dim bufferFrameCount As Integer = 0 : pAudioClient.GetBufferSize(bufferFrameCount)
Dim ppv As Object = Nothing : pAudioClient.GetService(IID_IAudioCaptureClient, ppv)
Dim pCaptureClient = CType(ppv, IAudioCaptureClient)
Dim buf = New Short(44100 * 10 * 2) {} ' ten seconds of audio
Dim nFrames = 0
pAudioClient.Start()

While True
    Await WaitForSingleObjectAsync(hEvent)
    Dim pData As IntPtr, NumFramesToRead As Integer = 0, dwFlags As Integer = 0
    pCaptureClient.GetBuffer(pData, NumFramesToRead, dwFlags, Nothing, Nothing)
    Dim nFramesToCopy = Math.Min(NumFramesToRead, buf.Length \ 2 - nFrames)
    Runtime.InteropServices.Marshal.Copy(pData, buf, nFrames * 2, nFramesToCopy * 2)
    pCaptureClient.ReleaseBuffer(NumFramesToRead)
    nFrames += nFramesToCopy
    If nFrames >= buf.Length \ 2 Then Exit While
End While

pAudioClient.Stop()
Runtime.InteropServices.Marshal.FinalReleaseComObject(pCaptureClient)
pCaptureClient = Nothing
CloseHandle(hEvent) : hEvent = Nothing

代码是基于事件的。它使用 Win32 事件句柄,该句柄是通过 Win32 函数 CreateEventEx 获取的。每次有新的音频数据缓冲区可用时,事件就会被设置。因为我向 CreateEventEx 传递了“0”作为标志,所以它是一个自动重置事件,也就是说,每次我成功等待它时,它都会被重置。下面是小的辅助函数 WaitForSingleObjectAsync,它允许我使用漂亮的 Await 语法。

Function WaitForSingleObjectAsync(hEvent As IntPtr) As Task
    Return Task.Run(Sub()
                        Dim r = WaitForSingleObjectEx(hEvent, &HFFFFFFFF, True)
                        If r <> 0 Then Throw New Exception("Unexpected event")
                    End Sub)
End Function

播放音频

这是播放音频的代码。同样,我一开始就有一个包含我所有十秒波形音频的缓冲区“buf”,然后播放所有内容。你可能希望使用较小的缓冲区并重复使用它们。

Dim hEvent = CreateEventEx(Nothing, Nothing, 0, EventAccess.EVENT_ALL_ACCESS)
pAudioClient.SetEventHandle(hEvent)
Dim bufferFrameCount As Integer = 0 : pAudioClient.GetBufferSize(bufferFrameCount)
Dim ppv As Object = Nothing : pAudioClient.GetService(IID_IAudioRenderClient, ppv)
Dim pRenderClient = CType(ppv, IAudioRenderClient)
Dim nFrame = 0
pAudioClient.Start()

While True
    Await WaitForSingleObjectAsync(hEvent)
    Dim numFramesPadding = 0 : pAudioClient.GetCurrentPadding(numFramesPadding)
    Dim numFramesAvailable = bufferFrameCount - numFramesPadding
    If numFramesAvailable = 0 Then Continue While
    Dim numFramesToCopy = Math.Min(numFramesAvailable, buf.Length \ 2 - nFrame)
    Dim pData As IntPtr = Nothing : pRenderClient.GetBuffer(numFramesToCopy, pData)
    Runtime.InteropServices.Marshal.Copy(buf, nFrame * 2, pData, numFramesToCopy * 2)
    pRenderClient.ReleaseBuffer(numFramesToCopy, 0)
    nFrame += numFramesToCopy
    If nFrame >= buf.Length \ 2 Then Exit While
End While

' and wait until the buffer plays out to the end...
While True
    Dim numFramesPadding = 0 : pAudioClient.GetCurrentPadding(numFramesPadding)
    If numFramesPadding = 0 Then Exit While
    Await Task.Delay(20)
End While

pAudioClient.Stop()
Runtime.InteropServices.Marshal.FinalReleaseComObject(pRenderClient) : pRenderClient = Nothing
CloseHandle(hEvent) : hEvent = Nothing

WASAPI 和 IAudioClient 的 P/Invoke 互操作库

剩下的是一个庞大的 P/Invoke 互操作库。我花了好几天时间才把所有这些东西拼凑起来。我绝不是 P/Invoke 专家。我敢打赌定义中存在错误,而且我确信它们没有体现最佳实践。

Module Interop
    <Runtime.InteropServices.DllImport("Mmdevapi.dll", ExactSpelling:=True, PreserveSig:=False)>
    Public Sub ActivateAudioInterfaceAsync(<Runtime.InteropServices.MarshalAs(_
           Runtime.InteropServices.UnmanagedType.LPWStr)> deviceInterfacePath As String, _
           <Runtime.InteropServices.MarshalAs(Runtime.InteropServices.UnmanagedType.LPStruct)> _
           riid As Guid, activationParams As IntPtr, completionHandler As _
           IActivateAudioInterfaceCompletionHandler, _
           ByRef activationOperation As IActivateAudioInterfaceAsyncOperation)
    End Sub

    <Runtime.InteropServices.DllImport("ole32.dll", _
                ExactSpelling:=True, PreserveSig:=False)>
    Public Sub PropVariantClear(ByRef pvar As PROPVARIANT)
    End Sub

    <Runtime.InteropServices.DllImport("kernel32.dll", _
           CharSet:=Runtime.InteropServices.CharSet.Unicode, _
           ExactSpelling:=False, PreserveSig:=True, SetLastError:=True)>
    Public Function CreateEventEx(lpEventAttributes As IntPtr, lpName As IntPtr, _
           dwFlags As Integer, dwDesiredAccess As EventAccess) As IntPtr
    End Function

    <Runtime.InteropServices.DllImport("kernel32.dll", _
             ExactSpelling:=True, PreserveSig:=True, SetLastError:=True)>
    Public Function CloseHandle(hObject As IntPtr) As Boolean
    End Function

    <Runtime.InteropServices.DllImport("kernel32", _
          ExactSpelling:=True, PreserveSig:=True, SetLastError:=True)>
    Function WaitForSingleObjectEx(hEvent As IntPtr, milliseconds _
          As Integer, bAlertable As Boolean) As Integer
    End Function


    Public ReadOnly PKEY_Device_FriendlyName As New PROPERTYKEY With _
           {.fmtid = New Guid("a45c254e-df1c-4efd-8020-67d146a850e0"), .pid = 14}
    Public ReadOnly PKEY_Device_DeviceDesc As New PROPERTYKEY With _
           {.fmtid = New Guid("a45c254e-df1c-4efd-8020-67d146a850e0"), .pid = 2}
    Public ReadOnly PKEY_AudioEndpoint_Supports_EventDriven_Mode As New _
           PROPERTYKEY With {.fmtid = New Guid("1da5d803-d492-4edd-8c23-e0c0ffee7f0e"), .pid = 7}
    Public ReadOnly IID_IAudioClient As New Guid("1CB9AD4C-DBFA-4c32-B178-C2F568A703B2")
    Public ReadOnly IID_IAudioClient2 As New Guid("726778CD-F60A-4eda-82DE-E47610CD78AA")
    Public ReadOnly IID_IAudioRenderClient As New Guid("F294ACFC-3146-4483-A7BF-ADDCA7C260E2")
    Public ReadOnly IID_IAudioCaptureClient As New Guid("C8ADBD64-E71E-48a0-A4DE-185C395CD317")


    <Runtime.InteropServices.ComImport, Runtime.InteropServices.Guid(_
       "BCDE0395-E52F-467C-8E3D-C4579291692E")> Class MMDeviceEnumerator
    End Class


    <Runtime.InteropServices.ComImport, Runtime.InteropServices.InterfaceType(_
       Runtime.InteropServices.ComInterfaceType.InterfaceIsIUnknown), _
       Runtime.InteropServices.Guid("A95664D2-9614-4F35-A746-DE8DB63617E6")>
    Public Interface IMMDeviceEnumerator
        Sub EnumAudioEndpoints(dataflow As EDataFlow, dwStateMask _
            As DeviceStateFlags, ByRef ppDevices As IMMDeviceCollection)
        Sub GetDefaultAudioEndpoint(dataflow As EDataFlow, role As ERole, ByRef ppDevice As IMMDevice)
        Sub GetDevice(<Runtime.InteropServices.MarshalAs(_
            Runtime.InteropServices.UnmanagedType.LPWStr)> pwstrId As String, ByRef ppDevice As IntPtr)
        Sub RegisterEndpointNotificationCallback(pClient As IntPtr)
        Sub UnregisterEndpointNotificationCallback(pClient As IntPtr)
    End Interface


    <Runtime.InteropServices.ComImport, Runtime.InteropServices.InterfaceType(_
       Runtime.InteropServices.ComInterfaceType.InterfaceIsIUnknown), _
       Runtime.InteropServices.Guid("0BD7A1BE-7A1A-44DB-8397-CC5392387B5E")>
    Public Interface IMMDeviceCollection
        Sub GetCount(ByRef pcDevices As Integer)
        Sub Item(nDevice As Integer, ByRef ppDevice As IMMDevice)
    End Interface


    <Runtime.InteropServices.ComImport, Runtime.InteropServices.InterfaceType(_
       Runtime.InteropServices.ComInterfaceType.InterfaceIsIUnknown), _
       Runtime.InteropServices.Guid("D666063F-1587-4E43-81F1-B948E807363F")>
    Public Interface IMMDevice
        Sub Activate(<Runtime.InteropServices.MarshalAs(_
            Runtime.InteropServices.UnmanagedType.LPStruct)> iid As Guid, _
            dwClsCtx As CLSCTX, pActivationParams As IntPtr, ByRef ppInterface As IAudioClient2)
        Sub OpenPropertyStore(stgmAccess As Integer, ByRef ppProperties As IPropertyStore)
        Sub GetId(<Runtime.InteropServices.MarshalAs(_
            Runtime.InteropServices.UnmanagedType.LPWStr)> ByRef ppstrId As String)
        Sub GetState(ByRef pdwState As Integer)
    End Interface


    <Runtime.InteropServices.ComImport, Runtime.InteropServices.InterfaceType(_
          Runtime.InteropServices.ComInterfaceType.InterfaceIsIUnknown), _
          Runtime.InteropServices.Guid("886d8eeb-8cf2-4446-8d02-cdba1dbdcf99")>
    Public Interface IPropertyStore
        'virtual HRESULT STDMETHODCALLTYPE GetCount(/*[out]*/ __RPC__out DWORD *cProps)
        Sub GetCount(ByRef cProps As Integer)
        'virtual HRESULT STDMETHODCALLTYPE GetAt(/*Runtime.InteropServices.In*/ 
        '   DWORD iProp, /*[out]*/ __RPC__out PROPERTYKEY *pkey)
        Sub GetAt(iProp As Integer, ByRef pkey As IntPtr)
        'virtual HRESULT STDMETHODCALLTYPE GetValue(/*Runtime.InteropServices.In*/
        '    __RPC__in REFPROPERTYKEY key, /*[out]*/ __RPC__out PROPVARIANT *pv)
        Sub GetValue(ByRef key As PROPERTYKEY, ByRef pv As PROPVARIANT)
        'virtual HRESULT STDMETHODCALLTYPE SetValue(/*Runtime.InteropServices.In*/ 
        '  __RPC__in REFPROPERTYKEY key, /*Runtime.InteropServices.In*/ __RPC__in REFPROPVARIANT propvar)
        Sub SetValue(ByRef key As PROPERTYKEY, ByRef propvar As IntPtr)
        'virtual HRESULT STDMETHODCALLTYPE Commit()
        Sub Commit()
    End Interface


    <Runtime.InteropServices.ComImport, Runtime.InteropServices.InterfaceType(_
       Runtime.InteropServices.ComInterfaceType.InterfaceIsIUnknown), _
       Runtime.InteropServices.Guid("1CB9AD4C-DBFA-4c32-B178-C2F568A703B2")>
    Public Interface IAudioClient
        Sub Initialize(ShareMode As AUDCLNT_SHAREMODE, StreamFlags As AUDCLNT_FLAGS, _
            hnsBufferDuration As Long, hnsPeriodicity As Long, ByRef _
            pFormat As WAVEFORMATEX, AudioSessionGuid As IntPtr)
        'virtual HRESULT STDMETHODCALLTYPE GetBufferSize(/*[out]*/ _Out_  UINT32 *pNumBufferFrames) = 0;
        Sub GetBufferSize(ByRef pNumBufferFrames As Integer)
        'virtual HRESULT STDMETHODCALLTYPE GetStreamLatency(/*[out]*/ _Out_  REFERENCE_TIME *phnsLatency) = 0;
        Sub GetStreamLatency(ByRef phnsLatency As Long)
        'virtual HRESULT STDMETHODCALLTYPE GetCurrentPadding(/*[out]*/ _Out_  UINT32 *pNumPaddingFrames) = 0;
        Sub GetCurrentPadding(ByRef pNumPaddingFrames As Integer)
        'virtual HRESULT STDMETHODCALLTYPE IsFormatSupported(/*[in]*/ _In_  
        '   AUDCLNT_SHAREMODE ShareMode, /*[in]*/ _In_  const WAVEFORMATEX *pFormat, 
        '   /*[unique][out]*/ _Out_opt_  WAVEFORMATEX **ppClosestMatch) = 0;
        Sub IsFormatSupported(ShareMode As AUDCLNT_SHAREMODE, ByRef pFormat _
                              As WAVEFORMATEX, ByRef ppClosestMatch As IntPtr)
        'virtual HRESULT STDMETHODCALLTYPE GetMixFormat(/*[out]*/ _Out_  WAVEFORMATEX **ppDeviceFormat) = 0;
        Sub GetMixFormat(ByRef ppDeviceFormat As IntPtr)
        'virtual HRESULT STDMETHODCALLTYPE GetDevicePeriod(/*[out]*/ _Out_opt_  
        '    REFERENCE_TIME *phnsDefaultDevicePeriod, /*[out]*/ 
        '    _Out_opt_  REFERENCE_TIME *phnsMinimumDevicePeriod) = 0;
        Sub GetDevicePeriod(ByRef phnsDefaultDevicePeriod As Long, ByRef phnsMinimumDevicePeriod As Long)
        'virtual HRESULT STDMETHODCALLTYPE Start( void) = 0;
        Sub Start()
        'virtual HRESULT STDMETHODCALLTYPE Stop( void) = 0;
        Sub [Stop]()
        'virtual HRESULT STDMETHODCALLTYPE Reset( void) = 0;
        Sub Reset()
        'virtual HRESULT STDMETHODCALLTYPE SetEventHandle(/*[in]*/ HANDLE eventHandle) = 0;
        Sub SetEventHandle(eventHandle As IntPtr)
        'virtual HRESULT STDMETHODCALLTYPE GetService(/*[in]*/ _In_  REFIID riid, /*[iid_is][out]*/ _Out_  void **ppv) = 0;
        Sub GetService(<Runtime.InteropServices.MarshalAs(_
            Runtime.InteropServices.UnmanagedType.LPStruct)> riid As Guid, _
            <Runtime.InteropServices.MarshalAs(Runtime.InteropServices.UnmanagedType.IUnknown)> ByRef ppv As Object)
    End Interface


    <Runtime.InteropServices.ComImport, Runtime.InteropServices.InterfaceType(_
                     Runtime.InteropServices.ComInterfaceType.InterfaceIsIUnknown), _
                     Runtime.InteropServices.Guid("726778CD-F60A-4eda-82DE-E47610CD78AA")>
    Public Interface IAudioClient2
        Sub Initialize(ShareMode As AUDCLNT_SHAREMODE, StreamFlags As AUDCLNT_FLAGS, _
            hnsBufferDuration As Long, hnsPeriodicity As Long, _
            ByRef pFormat As WAVEFORMATEX, AudioSessionGuid As IntPtr)
        Sub GetBufferSize(ByRef pNumBufferFrames As Integer)
        Sub GetStreamLatency(ByRef phnsLatency As Long)
        Sub GetCurrentPadding(ByRef pNumPaddingFrames As Integer)
        Sub IsFormatSupported(ShareMode As AUDCLNT_SHAREMODE, _
            ByRef pFormat As WAVEFORMATEX, ByRef ppClosestMatch As IntPtr)
        Sub GetMixFormat(ByRef ppDeviceFormat As IntPtr)
        Sub GetDevicePeriod(ByRef phnsDefaultDevicePeriod As Long, ByRef phnsMinimumDevicePeriod As Long)
        Sub Start()
        Sub [Stop]()
        Sub Reset()
        Sub SetEventHandle(eventHandle As IntPtr)
        Sub GetService(<Runtime.InteropServices.MarshalAs(_
            Runtime.InteropServices.UnmanagedType.LPStruct)> riid As Guid, _
            <Runtime.InteropServices.MarshalAs(Runtime.InteropServices.UnmanagedType.IUnknown)> ByRef ppv As Object)
        'virtual HRESULT STDMETHODCALLTYPE IsOffloadCapable(/*[in]*/ _In_  
        '   AUDIO_STREAM_CATEGORY Category, /*[in]*/ _Out_  BOOL *pbOffloadCapable) = 0;
        Sub IsOffloadCapable(Category As Integer, ByRef pbOffloadCapable As Boolean)
        'virtual HRESULT STDMETHODCALLTYPE SetClientProperties(/*[in]*/ _In_  
        '  const AudioClientProperties *pProperties) = 0;
        Sub SetClientProperties(pProperties As IntPtr)
        'virtual HRESULT STDMETHODCALLTYPE GetBufferSizeLimits(/*[in]*/ _In_  
        '   const WAVEFORMATEX *pFormat, /*[in]*/ _In_  BOOL bEventDriven, /*[in]*/ 
        '  _Out_  REFERENCE_TIME *phnsMinBufferDuration, /*[in]*/ _Out_  
        '  REFERENCE_TIME *phnsMaxBufferDuration) = 0;
        Sub GetBufferSizeLimits(pFormat As IntPtr, bEventDriven As Boolean, _
                 phnsMinBufferDuration As IntPtr, phnsMaxBufferDuration As IntPtr)
    End Interface
    <Runtime.InteropServices.ComImport, Runtime.InteropServices.InterfaceType(_
        Runtime.InteropServices.ComInterfaceType.InterfaceIsIUnknown), _
        Runtime.InteropServices.Guid("F294ACFC-3146-4483-A7BF-ADDCA7C260E2")>
    Public Interface IAudioRenderClient
        'virtual HRESULT STDMETHODCALLTYPE GetBuffer(/*[in]*/ _In_  UINT32 NumFramesRequested,
        '   /*[out]*/ _Outptr_result_buffer_(_Inexpressible_(
        '  "NumFramesRequested * pFormat->nBlockAlign"))  BYTE **ppData) = 0;
        Sub GetBuffer(NumFramesRequested As Integer, ByRef ppData As IntPtr)
        'virtual HRESULT STDMETHODCALLTYPE ReleaseBuffer(/*[in]*/ _In_  
        '   UINT32 NumFramesWritten, /*[in]*/ _In_  DWORD dwFlags) = 0;
        Sub ReleaseBuffer(NumFramesWritten As Integer, dwFlags As Integer)
    End Interface
    <Runtime.InteropServices.ComImport, Runtime.InteropServices.InterfaceType(_
        Runtime.InteropServices.ComInterfaceType.InterfaceIsIUnknown), _
        Runtime.InteropServices.Guid("C8ADBD64-E71E-48a0-A4DE-185C395CD317")>
    Public Interface IAudioCaptureClient
        'virtual HRESULT STDMETHODCALLTYPE GetBuffer(/*[out]*/ _Outptr_result_buffer_(
        '   _Inexpressible_("*pNumFramesToRead * pFormat->nBlockAlign"))  
        '   BYTE **ppData, /*[out]*/ _Out_  UINT32 *pNumFramesToRead, /*[out]*/_Out_ 
        '   DWORD *pdwFlags, /*[out]*/_Out_opt_  UINT64 *pu64DevicePosition, 
        '   /*[out]*/_Out_opt_  UINT64 *pu64QPCPosition) = 0;
        Sub GetBuffer(ByRef ppData As IntPtr, ByRef pNumFramesToRead As Integer, _
               ByRef pdwFlags As Integer, pu64DevicePosition As IntPtr, pu64QPCPosition As IntPtr)
        'virtual HRESULT STDMETHODCALLTYPE ReleaseBuffer(/*[in]*/ _In_  UINT32 NumFramesRead) = 0;
        Sub ReleaseBuffer(NumFramesRead As Integer)
        'virtual HRESULT STDMETHODCALLTYPE GetNextPacketSize(
        '       /*[out]*/ _Out_  UINT32 *pNumFramesInNextPacket) = 0;
        Sub GetNextPacketSize(ByRef pNumFramesInNextPacket As Integer)
    End Interface
    <Runtime.InteropServices.ComImport, Runtime.InteropServices.InterfaceType(_
      Runtime.InteropServices.ComInterfaceType.InterfaceIsIUnknown), _
      Runtime.InteropServices.Guid("41D949AB-9862-444A-80F6-C261334DA5EB")>
    Public Interface IActivateAudioInterfaceCompletionHandler
        'virtual HRESULT STDMETHODCALLTYPE ActivateCompleted(/*[in]*/ _In_  
        '   IActivateAudioInterfaceAsyncOperation *activateOperation) = 0;
        Sub ActivateCompleted(activateOperation As IActivateAudioInterfaceAsyncOperation)
    End Interface
    <Runtime.InteropServices.ComImport, Runtime.InteropServices.InterfaceType(_
       Runtime.InteropServices.ComInterfaceType.InterfaceIsIUnknown), _
       Runtime.InteropServices.Guid("72A22D78-CDE4-431D-B8CC-843A71199B6D")>
    Public Interface IActivateAudioInterfaceAsyncOperation
        'virtual HRESULT STDMETHODCALLTYPE GetActivateResult(/*[out]*/ _Out_  
        '  HRESULT *activateResult, /*[out]*/ _Outptr_result_maybenull_  IUnknown **activatedInterface) = 0;
        Sub GetActivateResult(ByRef activateResult As Integer, _
            <Runtime.InteropServices.MarshalAs(Runtime.InteropServices.UnmanagedType.IUnknown)> _
            ByRef activateInterface As Object)
    End Interface

    <Runtime.InteropServices.ComImport, Runtime.InteropServices.InterfaceType(_
       Runtime.InteropServices.ComInterfaceType.InterfaceIsIUnknown), _
       Runtime.InteropServices.Guid("94ea2b94-e9cc-49e0-c0ff-ee64ca8f5b90")>
    Public Interface IAgileObject
    End Interface

    <Runtime.InteropServices.StructLayout(Runtime.InteropServices.LayoutKind.Sequential, _
               Pack:=1)> Structure WAVEFORMATEX
        Dim wFormatTag As Short
        Dim nChannels As Short
        Dim nSamplesPerSec As Integer
        Dim nAvgBytesPerSec As Integer
        Dim nBlockAlign As Short
        Dim wBitsPerSample As Short
        Dim cbSize As Short
    End Structure


    <Runtime.InteropServices.StructLayout(Runtime.InteropServices.LayoutKind.Sequential, _
               Pack:=1)> Public Structure PROPVARIANT
        Dim vt As UShort
        Dim wReserved1 As UShort
        Dim wReserved2 As UShort
        Dim wReserved3 As UShort
        Dim p As IntPtr
        Dim p2 As Integer
        ReadOnly Property Value As Object
            Get
                Select Case vt
                    Case 31 : Return Runtime.InteropServices.Marshal.PtrToStringUni(p) ' VT_LPWSTR
                    Case Else
                        Throw New NotImplementedException
                End Select
            End Get
        End Property
    End Structure

    <Runtime.InteropServices.StructLayout(Runtime.InteropServices.LayoutKind.Sequential, _
                Pack:=1)> Public Structure PROPERTYKEY
        <Runtime.InteropServices.MarshalAs(Runtime.InteropServices.UnmanagedType.Struct)> Dim fmtid As Guid
        Dim pid As Integer
        Public Overrides Function ToString() As String
            Return "{" & fmtid.ToString() & "} " & pid.ToString()
        End Function
    End Structure

    Enum EDataFlow
        eRender = 0
        eCapture = 1
        eAll = 2
        EDataFlow_enum_count = 3
    End Enum

    Enum ERole
        eConsole = 0
        eMultimedia = 1
        eCommunications = 2
        ERole_enum_count = 3
    End Enum


    Enum StgmMode
        STGM_READ = 0
        STGM_WRITE = 1
        STGM_READWRITE = 2
    End Enum

    Enum AUDCLNT_SHAREMODE
        AUDCLNT_SHAREMODE_SHARED = 0
        AUDCLNT_SHAREMODE_EXCLUSIVE = 1
    End Enum

    <Flags> Enum DeviceStateFlags
        DEVICE_STATE_ACTIVE = 1
        DEVICE_STATE_DISABLED = 2
        DEVICE_STATE_NOTPRESENT = 4
        DEVICE_STATE_UNPLUGGED = 8
        DEVICE_STATEMASK_ALL = 15
    End Enum

    <Flags> Enum AUDCLNT_FLAGS
        AUDCLNT_STREAMFLAGS_CROSSPROCESS = &H10000
        AUDCLNT_STREAMFLAGS_LOOPBACK = &H20000
        AUDCLNT_STREAMFLAGS_EVENTCALLBACK = &H40000
        AUDCLNT_STREAMFLAGS_NOPERSIST = &H80000
        AUDCLNT_STREAMFLAGS_RATEADJUST = &H100000
        AUDCLNT_SESSIONFLAGS_EXPIREWHENUNOWNED = &H10000000
        AUDCLNT_SESSIONFLAGS_DISPLAY_HIDE = &H20000000
        AUDCLNT_SESSIONFLAGS_DISPLAY_HIDEWHENEXPIRED = &H40000000
    End Enum

    <Flags> Enum EventAccess
        STANDARD_RIGHTS_REQUIRED = &HF0000
        SYNCHRONIZE = &H100000
        EVENT_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED Or SYNCHRONIZE Or &H3
    End Enum

    <Flags> Enum CLSCTX
        CLSCTX_INPROC_SERVER = 1
        CLSCTX_INPROC_HANDLER = 2
        CLSCTX_LOCAL_SERVER = 4
        CLSCTX_REMOTE_SERVER = 16
        CLSCTX_ALL = CLSCTX_INPROC_SERVER Or CLSCTX_INPROC_HANDLER Or _
                     CLSCTX_LOCAL_SERVER Or CLSCTX_REMOTE_SERVER
    End Enum
End Module

注释

免责声明:尽管我在微软从事 VB/C# 语言团队的工作,但本文严格来说是我基于公开信息和实验的个人业余作品——它不属于我的专业领域,是在我自己的业余时间撰写的,并非代表微软的观点,微软和我和不对其准确性做任何声明。

© . All rights reserved.