本文的例子使用NAudio.CoreAudioApi实现,全部为C#代码
以下仅为个人理解,不一定都对,勿信~
阅读这个文档,最好具备C++知识,因为会用到Marshal命名空间进行指针操作
一、关于Windows Core Audio
Windows Core Audio API 是一种非常底层的音频API,上层应用为DirectSound或者WaveXXX接口等,WASAPI是其中的一部分。
Windows Core Audio API 构成
Multimedia Device API(MMDevice):表示系统中的音频设备节点(Audio Device Endpoint),Mmdeviceapi.h
Windows Audio Session API(WASAPI):用来操作音频设备节点之间的音频流(Audio Stream),Audioclient.h,Audiopolicy.h,Audiosessiontypes.h
DeviceTopology API:用来处理音频设备节点拓扑结构。可以设计音频设备节点之间的数据流向,Devicetopology.h
EndpointVolume API:当音频设备节点为声卡独占模式(Exclusive-mode)时,使用该API对Volume进行操作,Endpointvolume.h
以上头文件位置,XXXX\Windows SDK\include,由于效率问题,这些接口以COM形式出现,而不是.net或者.net freamwork
我的电脑中,通过VS安装Windows SDK后,头文件位置为C:\Program Files (x86)\Windows Kits\10\Include\10.0.22000.0\um
一些常见C#库都对其有映射,BASS.Net,NAudio。
以上接口调用顺序:
CoCreateInstance -> IMMDeviceEnumerator -> IMMDevice -> WASAPI DTAPI EVAPI
各种例子,Microsoft Learn: Build skills that open doors in your career,Windows SDK and emulator archive | Microsoft Developer
二、用户模式下的音频组件 User-Mode Audio Components
User Client:QQ音乐
User Mode:Core Audio(Audio Endpoint)、Direct Sound(High-Level API)
Kernel Mode:驱动(Audio Driver)、声音硬件(Audio Adapter)
Audio Endpoint Device:音频设备节点,对声音硬件的抽象,常见的声音硬件有喇叭、耳机、话筒、麦克风
三、Audio Endpoint Device
Audio Endpoint:表示一个声音设备
Audio Adapter:一个声音设备处理声音的流程,如A/D,D/A,输出,输入
MMDvice API用来处理和Audio Endpoint相关的操作。
实现方式:
C++
使用CoCreateInstance -> IMMDeviceEnumerator -> 其他API,包括:
IMMDevice、IMMEndpoint、IMMNotificationClient(热插拔设备)
NAudio
MMDeviceEnumerator -> MMDevice
获取到MMDevice后,调用Activate方法激活设备,随后调用WASAPI,DTAPI,EVAPI
获取到Audio Device后,就可以创建AudioClient实例进行WASAPI操作了
四、Audio Session
Audio Stream:音频流,可以为Recording 或者 Playing
Audio Session:每一个Audio Stream都对应一个Audio Stream,每一个Audio Session都对应一个Audio Device
与上文提到的IMMNotificationClient,对应Session也有事件处理接口IAudioSessionEvents,这些接口主要处理状态变化、音量变化、开始Session、结束Session等和声音处理相关的事件
获取到Audio Device后可以通过AudioSessionManager来确认系统中有哪些Session
五、Audio Stream
获取到Audio Device后,如果Activate成功,就可以得到AudioClient实例
通过AudioClient实例不仅可以获取到Audio Session,还可获取到Audio Stream
Audio Stream就是实际的音频数据,通过WASAPI对其进行操作
六、Audio Render Client 一个播放音频的例子
// 使用NAudio.WaveFileReader读取wav文件
// 使用FileStream也可以,不过需要自己解析Wave Chunk
// 例子中使用的wav文件为48000,32bit,2-channel,PCM
// 因此每一帧的大小 frame = 32bit / 8 * 2 = 8 bytes
var reader = new WaveFileReader(odl.FileName);
// 每次读取48000帧,也就是1秒钟
// 每帧大小为4bytes
int frame = 48000;
var bytes = new Byte[frame * 8];
reader.Read(bytes, 0, frame * 8);
// 以下为Core Audio部分,使用的是NAudio.CoreAudioAPI的C#映射
// 获取Core Audio Device遍历接口
var enumer = new NAudio.CoreAudioApi.MMDeviceEnumerator();
// 获取默认的Audio Device,Render表示输出设备,Multimedia表示多媒体设备
// 备注:Capture是录音设备,Console是交互设备,Communication是通讯设备
var device = enumer.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);
// 读取文件的WAVE配置,并初始化给输出设备
var format = reader.WaveFormat;
// Audio Device只是表示声音设备,并不是真正处理声音的抽象
// Audio Client才是处理声音的抽象
var audioClient = device.AudioClient;
audioClient.Initialize(
AudioClientShareMode.Shared,
AudioClientStreamFlags.None,
10000000,
0,
format,
Guid.Empty);
// 获取输出设备每次可以填充的音频缓冲区大小
// 单位是Frame,Frame与Byte的对应关系要根据WaveFormat计算
var bufferSize = audioClient.BufferSize;
// 获取输出设备
var renderclient = audioClient.AudioRenderClient;
// 填充第一帧音频输出缓冲区
// 由于Core Audio是COM接口,要用Marshal.Copy完成指针内存拷贝
var pData = renderclient.GetBuffer(frame);
Marshal.Copy(bytes, 0, pData, frame * 8);
renderclient.ReleaseBuffer(bufferSize, AudioClientBufferFlags.None);
// 开始播放音频
audioClient.Start();
// 以下为循环填充,否则只播放一次缓冲区数据
var sec = 1;
var stop = false;
while (!stop)
{
Thread.Sleep(1000);
reader.Read(bytes, 0, frame * 8);
pData = renderclient.GetBuffer(frame);
Marshal.Copy(bytes, 0, pData, frame * 8);
renderclient.ReleaseBuffer(bufferSize, AudioClientBufferFlags.None);
}
audioClient.Stop();
renderclient.Dispose();
renderclient = null;
audioClient.Dispose();
audioClient = null;
device.Dispose();
device = null;
七、Audio Capture 一个录音的例子
// 与Audio Render部分的逻辑相同
var device = this.captureComboBox.SelectedItem as NAudio.CoreAudioApi.MMDevice;
var format = device.AudioClient.MixFormat;
var writer = new WaveFileWriter("capture.wav", format);
var audioClient = device.AudioClient;
audioClient.Initialize(
AudioClientShareMode.Shared,
AudioClientStreamFlags.AutoConvertPcm,
10000000, // sec
0,
format,
Guid.Empty);
var bufferSize = audioClient.BufferSize;
var captureclient = audioClient.AudioCaptureClient;
var sec = 1;
audioClient.Start();
while (!stop)
{
// 0 表示没有录音
var packageSize = captureclient.GetNextPacketSize();
while (packageSize != 0)
{
int numFramestoRead = 0;
AudioClientBufferFlags flags = AudioClientBufferFlags.None;
IntPtr package = captureclient.GetBuffer(out numFramestoRead, out flags);
captureclient.ReleaseBuffer(numFramestoRead);
var length = numFramestoRead * 8;
var buffer = new byte[length];
Marshal.Copy(package, buffer, 0, length);
writer.Write(buffer, 0, length);
packageSize = captureclient.GetNextPacketSize();
}
}
writer.Close();
writer.Dispose();
writer = null;
audioClient.Stop();
captureclient.Dispose();
captureclient = null;
audioClient.Dispose();
audioClient = null;
device.Dispose();
device = null;