录音软件怎样拿到麦克风的声音?录屏软件又怎样录下电脑正在播放的音乐?

在 Windows 上,WASAPI(Windows Audio Session API)是一套管理音频流、读取或写入音频缓冲区的接口。 它可以用于播放,也可以用于捕获。本文聚焦捕获:把声音变成程序能够读取和处理的采样数据。


一、WASAPI 在音频链路中的位置

1. 捕获与播放是两个方向

通俗解释:播放是程序把音频交给系统;捕获是程序从系统取回音频。

方向程序做什么常见场景
播放(Render)向播放缓冲区写数据音乐播放器、游戏声音
捕获(Capture)从捕获缓冲区读数据麦克风录音、会议、录屏声音

WASAPI 本身不是 MP3 或 AAC 编码器,也不会自动把声音发到网络。捕获之后,你可以把数据保存为音频文件、分析音量、送入语音识别,或者编码后传输。它只完成这条链路中的音频输入输出部分。

2. 什么叫音频端点?

可以先把“端点”理解为 Windows 展示给应用的音频输入或输出设备。例如麦克风是输入端点,扬声器和耳机可以是输出端点。

默认设备不是唯一设备。 应用可以选择默认端点,也可以枚举设备,让用户指定 USB 麦克风或某副耳机。本文示例用默认设备,便于把注意力放在采集流程上。


二、WASAPI有两种使用模式

1. 共享模式与独占模式:怎样使用设备?

对比项共享模式(Shared)独占模式(Exclusive)
设备使用多个应用共享端点一个应用独占该端点
音频路径由 Windows 音频引擎协调处理绕过共享音频引擎的处理路径
格式选择需要符合共享流支持的格式需要符合设备支持的格式
对其他应用的影响便于同时运行多个音频应用可能使其他应用无法使用该端点
入门场景日常录音、会议、系统声音捕获有明确设备格式或时延需求的应用

通俗解释:共享像大家一起使用公共设施,由系统协调;独占像单独包下一个场地,控制更直接,但别人暂时用不了。

2. 输入捕获与回环捕获:声音从哪里来?

对比项输入捕获播放端点回环捕获(Loopback)
音源麦克风、线路输入等捕获端点选中的播放端点正在播放的音频
选择端点eCaptureeRender
初始化标志不加 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 枚举指定查输入端点还是播放端点
eCaptureflow 的一种取值输入端点,例如麦克风
eRenderflow 的另一种取值播放端点,例如耳机输出
eMultimedia设备角色参数选择“多媒体”角色对应的默认设备
&device用于接收结果的地址让方法把找到的设备接口写入 device

eMultimedia 不决定输入还是输出;方向已经由 flow 决定。另一个角色值 eCommunications 表示通信用途的默认设备,按应用需求选择。

再看音频流的三个配置选项:

图中缩写完整名称用在哪里表达的意思
SHAREDAUDCLNT_SHAREMODE_SHAREDInitialize 的第一个参数与其他应用共享设备
EVENTCALLBACKAUDCLNT_STREAMFLAGS_EVENTCALLBACK第二个参数中的标志使用事件通知驱动数据读取
LOOPBACKAUDCLNT_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 的六个位置不能随意调换:

位置正式参数名本例的值为什么这样填
1ShareModeAUDCLNT_SHAREMODE_SHARED使用共享模式
2StreamFlagsflags启用事件驱动;录播放声音时再加回环
3hnsBufferDuration0共享事件模式下请求系统选择所需的最小缓冲区时长,不表示分配零字节
4hnsPeriodicity0此初始化接口在共享模式下要求该参数为零,不表示线程不等待
5pFormatformat指向采样率、声道、采样表示等格式描述,不是音频数据本身
6AudioSessionGuidnullptr不传入自定义音频会话标识,使用默认行为;与网络会话无关

第 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 捕获的必选组件。学习采集时,可以先验证音频格式和保存结果,再逐步接入后续处理。