录音软件怎样拿到麦克风的声音?录屏软件又怎样录下电脑正在播放的音乐?
在 Windows 上,WASAPI(Windows Audio Session API)是一套管理音频流、读取或写入音频缓冲区的接口。 它可以用于播放,也可以用于捕获。本文聚焦捕获:把声音变成程序能够读取和处理的采样数据。
一、WASAPI 在音频链路中的位置
1. 捕获与播放是两个方向
通俗解释:播放是程序把音频交给系统;捕获是程序从系统取回音频。
| 方向 | 程序做什么 | 常见场景 |
|---|---|---|
| 播放(Render) | 向播放缓冲区写数据 | 音乐播放器、游戏声音 |
| 捕获(Capture) | 从捕获缓冲区读数据 | 麦克风录音、会议、录屏声音 |
WASAPI 本身不是 MP3 或 AAC 编码器,也不会自动把声音发到网络。捕获之后,你可以把数据保存为音频文件、分析音量、送入语音识别,或者编码后传输。它只完成这条链路中的音频输入输出部分。
2. 什么叫音频端点?
可以先把“端点”理解为 Windows 展示给应用的音频输入或输出设备。例如麦克风是输入端点,扬声器和耳机可以是输出端点。
默认设备不是唯一设备。 应用可以选择默认端点,也可以枚举设备,让用户指定 USB 麦克风或某副耳机。本文示例用默认设备,便于把注意力放在采集流程上。
二、WASAPI有两种使用模式
1. 共享模式与独占模式:怎样使用设备?
| 对比项 | 共享模式(Shared) | 独占模式(Exclusive) |
|---|---|---|
| 设备使用 | 多个应用共享端点 | 一个应用独占该端点 |
| 音频路径 | 由 Windows 音频引擎协调处理 | 绕过共享音频引擎的处理路径 |
| 格式选择 | 需要符合共享流支持的格式 | 需要符合设备支持的格式 |
| 对其他应用的影响 | 便于同时运行多个音频应用 | 可能使其他应用无法使用该端点 |
| 入门场景 | 日常录音、会议、系统声音捕获 | 有明确设备格式或时延需求的应用 |
通俗解释:共享像大家一起使用公共设施,由系统协调;独占像单独包下一个场地,控制更直接,但别人暂时用不了。
2. 输入捕获与回环捕获:声音从哪里来?
| 对比项 | 输入捕获 | 播放端点回环捕获(Loopback) |
|---|---|---|
| 音源 | 麦克风、线路输入等捕获端点 | 选中的播放端点正在播放的音频 |
| 选择端点 | eCapture | eRender |
| 初始化标志 | 不加 LOOPBACK | 加 AUDCLNT_STREAMFLAGS_LOOPBACK |
| 设备使用模式 | 可根据需求使用共享或独占 | 本文讨论的端点回环要求共享模式 |

回环捕获直接取得音频流,不需要麦克风对着扬声器录音。这里讨论的是指定播放端点的混合声音,不代表能够自动取得所有设备的声音,也不等同于单独捕获某一个进程。
端点回环捕获只支持共享模式。 它与声卡可能提供的“立体声混音”录音设备也不是同一套入口,不要求用户先启用这样的硬件录音设备。
3. 轮询与事件驱动:什么时候去读?
| 方式 | 工作过程 | 需要关注什么 |
|---|---|---|
| 1. 定时查询 / 轮询 | 等一段时间,检查是否有可读数据 | 查询间隔过短会增加唤醒,过长可能积压 |
| 2. 事件驱动 | 等待系统事件,收到通知后读取可用包 | 需要设置事件句柄,并能响应停止请求 |

事件驱动通过 AUDCLNT_STREAMFLAGS_EVENTCALLBACK 和 SetEventHandle 配置。这里的“事件”是 Windows 同步事件,不是系统自动调用自己写的 C++ 回调函数;一般由工作线程等待事件后主动读取。
三、音频格式:捕获到的字节到底是什么?
1. 捕获格式说明
声音经过采样后变成一连串数值。常见表示有整数 PCM 和 IEEE 浮点采样;它们都不同于 AAC、MP3 这样的压缩数据。解释数据之前,必须知道采样率、声道数、采样表示及存储位数。
| 参数 | 常见字段 | 说明 |
|---|---|---|
| 采样率 | nSamplesPerSec | 每个声道每秒有多少次采样 |
| 声道数 | nChannels | 同时记录多少路声音 |
| 每个采样容器位数 | wBitsPerSample | 一个采样值占多少存储空间 |
| 格式标识 | wFormatTag / SubFormat | 字节按整数还是浮点等方式解释 |
| 每帧字节数 | nBlockAlign | 一个采样时刻的全部声道占多少字节 |
WAVEFORMATEX 保存基本格式;WAVEFORMATEXTENSIBLE 还可以描述子格式、有效位数和声道布局。容器位数不一定等于有效精度,例如采样值可以放在比有效位数更大的容器中。
2. 如何选择捕获格式?
选择捕获格式,就是确定:接下来读到的声音,以什么采样率、多少声道、哪种数值表示交给程序。
这里先区分两件事:
- 捕获格式:WASAPI 把音频交给程序时使用的格式。
- 后续处理格式:文件、编码器或语音识别模块希望接收的格式。
两者可以相同,也可以不同。比如系统交给你的是“48 kHz、双声道、32 位浮点”,语音识别模块要求的却是“16 kHz、单声道、16 位整数”。这种情况下,需要选择合适的捕获格式,再进行必要的转换。
① 没有特殊要求:先用系统的混合格式
共享模式下,Windows 音频引擎使用一种格式进行内部处理,这就是混合格式。GetMixFormat 是 IAudioClient 提供的一个查询方法,意思是:“请告诉我这个端点的音频引擎现在使用什么格式。”
它返回的是格式说明,不是音频数据,也不是开始录音的命令。假设已经取得音频客户端接口 audioClient,调用示意如下:
WAVEFORMATEX* format = nullptr; // 用来接收系统返回的格式说明
HRESULT hr = audioClient->GetMixFormat(&format);
if (FAILED(hr)) {
// 查询失败:处理错误,不能继续使用 format。
return;
}
// 查询成功后,可读取这些字段:
// format->nSamplesPerSec:采样率
// format->nChannels:声道数
// format->wBitsPerSample:每个采样容器的位数
// format->wFormatTag:格式类型;扩展格式还要看 SubFormat
// 后续把 format 传给同一设备的共享流 Initialize。
// 完成相关使用后,用 CoTaskMemFree(format) 释放这份格式说明。
这段是调用片段,不是完整录音程序。其中 format 是接收结果的指针变量,&format 表示让方法把结果地址写回这个变量;hr 是调用结果,FAILED(hr) 用来判断是否失败。
例如,查询结果可能是:
| 查询到的信息 | 示例值 | 读取时意味着什么 |
|---|---|---|
| 采样率 | 48000 Hz | 每个声道每秒有 48000 个采样值 |
| 声道数 | 2 | 每个音频帧包含两个声道的值 |
| 采样表示 | IEEE 32 位浮点 | 按浮点数解释数据,不能当作 16 位整数读取 |
| 每帧字节数 | 8 | 两个声道,每个值 4 字节 |
② 想指定格式:先询问是否支持
假如你希望直接取得“48 kHz、双声道、16 位整数 PCM”,就需要先构造描述这个要求的格式结构,再调用 IsFormatSupported。
这两个方法的区别是:GetMixFormat 问“系统现在用什么”,IsFormatSupported 问“我希望用这个,可不可以”。
下面是共享模式的查询示意。假定 requestedFormat 已完整填写为你希望使用的格式:
WAVEFORMATEX* closest = nullptr;
HRESULT hr = audioClient->IsFormatSupported(
AUDCLNT_SHAREMODE_SHARED, // 准备使用的设备模式
&requestedFormat, // 希望使用的格式说明
&closest // 接收系统建议的接近格式
);
结果要分情况处理:
| 返回结果 | 意思 | 下一步 |
|---|---|---|
S_OK | 支持你提出的格式 | 可以把请求格式交给 Initialize,仍须检查初始化结果 |
S_FALSE | 原格式不完全匹配,但提供了接近格式 | 检查 closest 的实际内容,再决定接受它还是采用其他方案 |
AUDCLNT_E_UNSUPPORTED_FORMAT | 不支持请求格式,也没有可用的接近匹配 | 换一种格式,或采用受支持格式捕获后自行转换 |
| 其他失败结果 | 可能是设备失效、参数错误等 | 按错误原因处理,不应一律当作格式不支持 |
注意:S_FALSE 不是失败码,不能只用 SUCCEEDED(hr) 就认定“原格式受支持”。 如果系统返回了 closest,用完也要通过 CoTaskMemFree 释放。
查询独占模式时,第一个参数改为 AUDCLNT_SHAREMODE_EXCLUSIVE,第三个参数传 nullptr;需要测试设备实际接受的格式,不能直接假设共享混合格式适用。
四、核心接口:找到设备,然后打开音频流
| 接口 | 职责 | 可以怎样理解 |
|---|---|---|
IMMDeviceEnumerator | 枚举或查找默认端点 | 设备目录 |
IMMDevice | 表示选中的端点 | 选中的设备入口 |
IAudioClient | 初始化、启动、停止流 | 音频流管理器 |
IAudioCaptureClient | 取得与释放捕获包 | 数据读取接口 |
前两个属于设备管理接口,后两个负责 WASAPI 音频流。它们配合使用,不是同一个对象换了几个名字。

五、初始化:一步步准备好音频采集
以“共享模式 + 事件驱动”为例。先不用记 API 的名字,把整个准备过程理解成:找音源、确定数据格式、设置通知方式,最后开始录音。
1. 先分清:对象、方法和参数不是一回事
| 类别 | 例子 | 说明 |
|---|---|---|
| 接口:提供一组操作能力 | IAudioClient | 音频流的“控制面板”,可以设置、启动、停止 |
| 方法:让接口执行某个动作 | Initialize、Start | 控制面板上的“设置”和“开始”按钮 |
| 参数或选项:说明动作怎么执行 | AUDCLNT_SHAREMODE_SHARED | 告诉系统“这次使用共享模式” |
| 变量:保存程序中的对象或信息 | audioClient、format | 保存控制面板接口、音频格式等内容的名字 |
例如 audioClient->Start() 的意思就是:通过 audioClient 保存的音频接口,执行“开始”操作。 Start 不是参数,也不是一种音频模式。
2. 先看流程,再对应 API

① 准备使用 Windows 接口的环境
这里的音频接口通过 COM 提供。COM 可以先理解为 Windows 提供和管理接口对象的一套机制。在线程上使用这些接口前,需要先完成相应初始化,例如调用 CoInitializeEx。这一步准备的是接口运行环境,还没有开始录音。
② 找到要录制的声音来源
先创建一个设备枚举器 IMMDeviceEnumerator,它负责“查设备列表、找默认设备”。选好后得到 IMMDevice,代表这次要使用的具体端点。
- 录麦克风:找输入端点。
- 录电脑播放的声音:找对应的播放端点,并在后面启用回环捕获。
③ 取得管理音频流的接口
在选中的设备上调用 Activate,请求得到 IAudioClient。后面通过它设置音频流、启动采集和停止采集。
可以理解为:第二步选中了设备,第三步拿到了操作这个设备上音频流的“控制面板”。Activate 本身不等于开始录音。
④ 问清楚:音频数据是什么格式?
调用 GetMixFormat,取得共享音频引擎使用的格式,包括采样率、声道数和采样表示等。将结果保存在 format 这样的变量中,后面初始化和解释数据都会用到它。
入门时先使用返回的混合格式。若想换成其他格式,需要另外检查支持情况;GetMixFormat 自身不会替你协商任意格式。
⑤ 告诉系统:这次怎样采集?
调用 Initialize 配置音频流。这里要表达三个意思:
- 共享使用设备:选共享模式。
- 有数据时通知我:打开事件驱动选项。
- 如果录的是播放声音:再打开回环选项。
上一版图中的 SHARED、EVENTCALLBACK、LOOPBACK 就是这三个选项的缩写,不是额外的三个处理步骤。完整名称和用法见下一节。
⑥ 取得真正读取数据的接口
通过 IAudioClient 的 GetService 方法取得 IAudioCaptureClient。
两个接口的分工是:IAudioClient 负责“设置、开、关”,IAudioCaptureClient 负责“取出一包数据、用完归还”。
⑦ 告诉系统:用哪个通知信号叫醒我?
程序先创建一个 Windows 事件对象,再调用 SetEventHandle 把它交给音频系统。可以把事件对象理解为“通知铃”,把它的句柄理解为系统用来找到这只铃的标识。
有数据可处理时,系统发出这个事件信号;等待它的线程被唤醒,然后自己调用读取接口。这里不是注册一个让系统自动调用的 C++ 函数。
⑧ 准备退出信号和自己的存储空间
除了“有数据了”的通知,通常再创建一个“该停止了”的事件,确保等待中的线程也能及时退出。
应用还需要准备自己的缓冲区或队列,保存从系统复制出来的数据,方便后续写文件、分析或编码。这属于应用的组织方式,不是一个名为“准备缓冲区”的 WASAPI 方法。
⑨ 开始采集,并等待数据
调用 Start 才正式启动音频流。此后采集线程等待数据通知,醒来后读取可用包;收到停止通知时退出。下一章会展开“读取一包数据”具体做什么。
串起来记:准备环境 → 选设备 → 拿控制接口 → 查格式 → 做配置 → 拿读取接口 → 设置通知 → 准备退出与缓存 → 开始采集。
3. 代码里的选项分别是什么意思?
先看“选择设备”这一句:
enumerator->GetDefaultAudioEndpoint(flow, eMultimedia, &device);
| 名称 | 是什么 | 这一句中的意思 |
|---|---|---|
enumerator | 保存设备枚举接口的变量 | 通过它查找设备 |
GetDefaultAudioEndpoint | 方法名 | 获取某类默认音频端点 |
flow | 输入参数,类型是 EDataFlow 枚举 | 指定查输入端点还是播放端点 |
eCapture | flow 的一种取值 | 输入端点,例如麦克风 |
eRender | flow 的另一种取值 | 播放端点,例如耳机输出 |
eMultimedia | 设备角色参数 | 选择“多媒体”角色对应的默认设备 |
&device | 用于接收结果的地址 | 让方法把找到的设备接口写入 device |
eMultimedia 不决定输入还是输出;方向已经由 flow 决定。另一个角色值 eCommunications 表示通信用途的默认设备,按应用需求选择。
再看音频流的三个配置选项:
| 图中缩写 | 完整名称 | 用在哪里 | 表达的意思 |
|---|---|---|---|
SHARED | AUDCLNT_SHAREMODE_SHARED | Initialize 的第一个参数 | 与其他应用共享设备 |
EVENTCALLBACK | AUDCLNT_STREAMFLAGS_EVENTCALLBACK | 第二个参数中的标志 | 使用事件通知驱动数据读取 |
LOOPBACK | AUDCLNT_STREAMFLAGS_LOOPBACK | 第二个参数中的标志 | 从播放端点回环捕获声音 |
共享模式和另外两个标志不在同一个参数里。 事件标志与回环标志可以用 C++ 的按位或运算符 | 组合,表示同时启用;不能把图上的加号当成实际代码写法。
4. 配置阶段
下面仍是示意片段,不是可直接运行的完整程序。假定已经初始化 COM、创建枚举器,并准备好接收接口的变量;实际调用时必须逐步检查返回值。
// loopback 是应用自己定义的布尔变量,不是 WASAPI 的固定参数名。
const bool loopback = true; // true 录播放声音;false 录输入端点
EDataFlow flow = loopback ? eRender : eCapture;
enumerator->GetDefaultAudioEndpoint(flow, eMultimedia, &device);
// 此处省略:通过 device->Activate 取得 audioClient,
// 并通过 audioClient->GetMixFormat 取得 format。
// DWORD 是 Windows 常用的 32 位无符号整数类型,用于存放标志位。
DWORD flags = AUDCLNT_STREAMFLAGS_EVENTCALLBACK;
if (loopback) {
flags |= AUDCLNT_STREAMFLAGS_LOOPBACK; // 保留事件标志,再加回环标志
}
HRESULT hr = audioClient->Initialize(
AUDCLNT_SHAREMODE_SHARED, // 1. 设备使用模式
flags, // 2. 本次启用的功能标志
0, // 3. 请求的缓冲区时长
0, // 4. 请求的设备处理周期
format, // 5. 音频格式描述
nullptr // 6. 不指定自定义会话标识
);
Initialize 的六个位置不能随意调换:
| 位置 | 正式参数名 | 本例的值 | 为什么这样填 |
|---|---|---|---|
| 1 | ShareMode | AUDCLNT_SHAREMODE_SHARED | 使用共享模式 |
| 2 | StreamFlags | flags | 启用事件驱动;录播放声音时再加回环 |
| 3 | hnsBufferDuration | 0 | 共享事件模式下请求系统选择所需的最小缓冲区时长,不表示分配零字节 |
| 4 | hnsPeriodicity | 0 | 此初始化接口在共享模式下要求该参数为零,不表示线程不等待 |
| 5 | pFormat | format | 指向采样率、声道、采样表示等格式描述,不是音频数据本身 |
| 6 | AudioSessionGuid | nullptr | 不传入自定义音频会话标识,使用默认行为;与网络会话无关 |
第 3、4 个参数的时间单位是 100 纳秒,但本例都传入特殊值 0,不需要自行换算。音频会话用于把相关音频流归组管理,初学时无需为这个示例单独创建会话标识。
返回值 hr 的类型是 HRESULT,它是本次调用的结果码。用 FAILED(hr) 等方式判断是否失败。初始化成功也不代表已经录音:还要取得读取接口、设置事件,最后调用 Start。
六、读取音频包:借用、处理、归还
1. 每次唤醒要做什么?
收到事件不代表“恰好只有一个包”。一次唤醒后可能积累了多个包,应循环处理当前可用数据,再回去等待。

2. GetBuffer 返回什么?
| 输出 | 用途 |
|---|---|
数据指针pData | 指向当前捕获包 |
| 音频帧数 | 用于计算数据字节数和持续时长 |
flags | 标记静音、数据不连续或时间戳异常等状态 |
| 可选设备位置与时间信息 | 用于定位采样在时间轴上的位置 |
GetBuffer 与对应的 ReleaseBuffer 必须在同一个线程上成对使用。一般消费整包并归还整包帧数,不要把一次读取当作可以随意切割的字节流操作。
3. 为什么不能一直保存 pData?
通俗解释:系统把一块内存暂时借给你读取,ReleaseBuffer 表示用完归还。这个指针不因此变成应用自己的长期存储。

可以在归还前直接完成短小处理;如果要交给另一个线程,就先复制到自己持有的内存。不要只把原始指针放入队列,再归还系统缓冲区。
4. 静音、无包和数据不连续不是一回事
| 状态 | 表示什么 | 应怎样理解 |
|---|---|---|
| 当前没有包 | 这次没有可读数据 | 回到等待,不要忙循环 |
AUDCLNT_BUFFERFLAGS_SILENT | 这批帧代表静音 | 按帧数产生对应时长的静音数据,不依赖原始数据内容 |
AUDCLNT_BUFFERFLAGS_DATA_DISCONTINUITY | 数据流发生不连续 | 记录情况,按业务要求处理时间线 |
AUDCLNT_BUFFERFLAGS_TIMESTAMP_ERROR | 当前包的时间信息存在问题 | 不盲目信任该包的时间戳 |
这些状态是接口返回信息的一部分,不能只复制字节而忽略标志。
对于常见的有符号整数 PCM 和 IEEE 浮点格式,零值可以表示静音;8 位无符号 PCM 的静音中点则是 128。填充方式必须与实际格式一致。
简记:没有包是“现在无数据”,静音包是“这段时间有数据,但声音幅度为零”。两者对时间线的意义不同。
七、从“能录到”到“稳定地录”
1. 采集线程应保持轻量
采集线程适合做:等待事件、读取状态、复制必要的数据、归还系统缓冲区。文件写入、编码和网络传输可以放到后续线程,避免长时间占用读取路径。

队列需要明确同步方式和容量限制。只管往里堆数据,可能使内存持续增长,也可能让播放或传输越来越滞后。
2. 为什么还需要应用缓冲区?
系统每次返回的包大小,未必等于编码器或算法每次需要的数据量。例如,假设每包 480 帧,而处理器每次要 1024 帧,就需要累计、拼接,再按要求取出。这里是说明机制的数字示例,不是 WASAPI 固定的包大小。
延迟还取决于队列中积压的音频时长。例如 48 kHz 下积压 4800 帧,仅排队就增加约 100 ms。线程每隔 1 ms 查询一次,不代表端到端延迟只有 1 ms。
3. 停止时需要唤醒等待线程
事件驱动线程可能正在阻塞等待。如果退出时只改一个布尔值,线程不一定马上醒来。可以同时等待“音频就绪事件”和“停止事件”,由停止事件触发退出。

一种便于管理的安排是:控制线程发停止信号;采集线程完成当前包、停止音频流、清理接口,然后退出;控制线程再完成 join。不要在线程仍访问接口时提前销毁接口。
GetMixFormat 分配的格式内存需用 CoTaskMemFree 释放。COM 的成功初始化与反初始化应在对应线程配对;事件句柄也需要关闭。
4. 设备变化需要重新处理
拔掉 USB 麦克风、禁用设备或重配置端点,都可能使音频流失效。收到设备失效错误时,应停止当前路径,根据需求重新选择设备并初始化。
默认设备发生变化,也不意味着已打开的流会自动切过去。如果产品需要“始终跟随默认设备”,应设计设备变化通知与流重建逻辑。
八、捕获之后:保存、分析或传输

| 需求 | 后续还要做什么 |
|---|---|
| 保存录音 | 按文件格式写入格式信息与音频数据,例如 WAV |
| 显示音量或波形 | 正确解释采样值,计算幅度或生成绘图数据 |
| 语音识别 | 按识别接口要求转换采样率、声道与采样格式 |
| 网络通话或直播 | 编码、传输、接收端解码和播放,并处理时间与缓存 |
AAC、Opus 是编码选择,RTMP 等是传输方案;它们都不是 WASAPI 捕获的必选组件。学习采集时,可以先验证音频格式和保存结果,再逐步接入后续处理。