返回

Windows音频闪避:让应用通话时自动降低其他音量

windows

让 Windows 知道你的应用正在通话:实现音频自动闪避

不少开发者会遇到一个情况:开发的应用程序带有通话功能,比如 VoIP 软件、在线会议工具等。希望当用户的应用开始或接听通话时,Windows 系统能够自动降低其他应用程序的音量,就像系统自带的那个声音设置选项一样:

Windows 通信活动声音设置界面

这个功能通常被称为“音频闪避”(Audio Ducking)或“音量衰减”。但问题是,怎么才能让 Windows “知道”你的应用正在进行通信活动,从而触发这个效果呢?是不是需要调用某个特殊的 API?或者用某种特定的方式创建音频流?

如果你翻阅过 WASAPI(Windows Audio Session API)或者 Core Audio API 的文档,可能一时半会儿找不到直接的答案。WASAPI 里有个“独占模式”(Exclusive Mode)音频流,听起来有点像,但它太“霸道”了——直接静音所有其他应用的声音,而不是我们想要的“降低音量”。

别急,这事儿能办。

问题出在哪?Windows 为啥不知道你在“打电话”?

简单来说,Windows 不会自动“侦测”某个应用是不是在打电话。系统需要应用程序主动“告知”它,当前正在处理的音频流属于“通信”类别。用户在声音设置里的选择(降低 80%、降低 50%、静音或不执行任何操作),实际上是为标记为“通信”的音频流预设的行为。

如果你的应用只是普普通通地创建了一个音频流来播放或录制声音,Windows 会把它当作一般的媒体播放或者录音任务。它不会知道这个音频流是用于电话会议、语音聊天还是仅仅是播放个背景音乐。

所以,关键不在于创建流的方式有多特别,而在于给这个流打上正确的“标签”。

解决方案:给音频会话“打标签”

要实现这个功能,我们需要动用 Windows 的 Core Audio API,具体来说是 IAudioSessionControl 接口,以及它的增强版 IAudioSessionControl2

核心思路是:获取应用当前使用的音频会话(Audio Session),然后设置该会话的类别(Category)为“通信”(Communications)。

核心武器:IAudioSessionControl2AudioCategory_Communications

IAudioSessionControl2 接口提供了一个方法 SetGroupingParam,但我们这里关注的是它对“会话类别”(Session Category)的支持。虽然设置类别看起来不是直接通过 SetGroupingParam 完成,但在概念上,是通过获取 IAudioSessionControl 并查询 IAudioSessionControl2,然后利用音频会话管理机制,在创建流时或者稍后对其进行配置,间接影响系统如何识别这个会话。更准确地说,是通过 IAudioSessionControl::SetDisplayNameIAudioSessionControl::SetIconPath 这些方法可以给会话提供一些元数据,而更底层地,应用程序可以通过 IMMDevice::Activate 激活音频客户端时,或者在使用更现代的 API (如 AudioGraph for UWP/WinUI) 时指定流的类别。

不过,针对传统 Win32 桌面应用,利用 IAudioSessionManager2 来控制会话属性是常用方式。我们需要获取与你的音频流关联的 IAudioSessionControl,然后查询(QueryInterface)得到 IAudioSessionControl2。虽然 IAudioSessionControl2 本身没有直接的 SetCategory 方法,但它与系统的音频策略紧密相关。系统通过检查会话的属性(可能包括由 IAudioSessionControl 设置的信息以及流创建时的参数)来判断其类别。

实际上,更直接关联到“通信类别”从而触发音量闪避的机制,是在创建音频流时 指定流的意图或类别。例如,在使用 WASAPI 时,虽然没有直接参数让你在 IAudioClient::Initialize 时指定类别,但系统会根据某些因素推断。

一个被证实可行且常用的方法,是利用 PolicyConfig.h 头文件中的一个非公开 (但广泛使用)的 COM 接口 IPolicyConfigVista 或类似接口来设置进程默认的音频设备角色,但这通常用于设置默认的 通信设备,而不是直接标记某个 特定流 的类别为通信。

然而,根据微软官方文档和社区实践,最符合设计意图且公开推荐的方式,是使用 IAudioClient 激活时传递的 AUDCLNT_STREAMFLAGS。虽然文档没有明确指出哪个 flag 直接触发 音量闪避,但可以肯定的是,与系统集成的关键在于正确配置音频会话。

一个被开发者社区实践证明有效的方法,是利用 IAudioSessionControl 本身。虽然它不直接设置“通信类别”,但当一个音频会话被系统识别为使用了“通信”设备(通常是用户在声音设置里指定的“默认通信设备”)时,该会话的行为就更倾向于被系统视为通信会话。

让我们聚焦于最核心、最可能直接关联的机制:使用 IAudioSessionControl 的相关功能和确保音频流绑定到正确的设备角色。

1. 获取 IAudioSessionManager2

首先,你需要获取音频设备的 IAudioSessionManager2 接口。这通常从 IMMDevice 开始。

#include <mmdeviceapi.h>
#include <audiopolicy.h>
#include <endpointvolume.h> // 引入 AudioSessionManager 的头文件

// 假设你已经有了一个 IMMDevice 指针 pDevice 指向你的音频输出设备
// (获取 IMMDevice 的代码通常涉及 IMMDeviceEnumerator)

IMMDevice *pDevice = nullptr;
IAudioSessionManager2 *pSessionManager = nullptr;
HRESULT hr;

// ... 获取 pDevice 的代码 ...
// 例如,使用 IMMDeviceEnumerator::GetDefaultAudioEndpoint(eRender, eCommunications, &pDevice);
// 获取默认的通信渲染设备是推荐做法

hr = pDevice->Activate(
    __uuidof(IAudioSessionManager2),
    CLSCTX_ALL,
    NULL,
    (void**)&pSessionManager
);

if (SUCCEEDED(hr)) {
    // 成功获取 Session Manager
} else {
    // 处理错误
    if (pDevice) pDevice->Release();
    // ... 其他清理 ...
    return; // 或者抛出异常
}

// 别忘了在使用完后 Release 接口指针
// pSessionManager->Release();
// pDevice->Release();

重点: 在获取 IMMDevice 时,尝试使用 eCommunications 角色 (IMMDeviceEnumerator::GetDefaultAudioEndpoint(eRender, eCommunications, &pDevice)) 来获取默认的通信设备。使用通信设备创建的音频流,天然就更容易被 Windows 识别为通信活动。

2. 获取特定会话的 IAudioSessionControl

接下来,你需要拿到属于你应用程序的那个音频流的 IAudioSessionControl。如果你是刚创建 IAudioClient 并初始化,可以通过 IAudioClient::GetService 来获取。

#include <audioclient.h>

// 假设你已经有了一个 IAudioClient 指针 pAudioClient
// (pAudioClient 通常在初始化音频流时获得)

IAudioClient *pAudioClient = nullptr;
IAudioSessionControl *pSessionControl = nullptr;
HRESULT hr;

// ... 初始化 pAudioClient 的代码 ...
// hr = pDevice->Activate(__uuidof(IAudioClient), CLSCTX_ALL, NULL, (void**)&pAudioClient);
// hr = pAudioClient->Initialize(...);

hr = pAudioClient->GetService(__uuidof(IAudioSessionControl), (void**)&pSessionControl);

if (SUCCEEDED(hr)) {
    // 成功获取 Session Control
} else {
    // 处理错误
    if (pAudioClient) pAudioClient->Release();
    // ... 其他清理 ...
    // (可能需要在获取失败时,从 IAudioSessionManager2 获取,但这更复杂)
    return;
}

// 使用完后 Release
// pSessionControl->Release();
// pAudioClient->Release(); // 如果不再需要

3. (关键步骤存疑 - 理论联系实际) 利用会话属性暗示“通信”意图

虽然没有直接的 SetCategory(AudioCategory_Communications) 公开 API 调用,但通过以下组合拳,可以强烈暗示 给系统你的会话是用于通信的:

  • 使用通信设备: 如步骤 1 所述,优先在默认通信设备上创建音频流。
  • 设置会话显示信息: 使用 IAudioSessionControl::SetDisplayNameIAudioSessionControl::SetIconPath 为你的音频会话设置有意义的名称和图标(比如应用名+“通话”,或者一个电话图标)。这虽然主要是给用户看的(例如在音量合成器里),但也是向系统提供元数据的一种方式。
// 假设已经获取了 pSessionControl

// 设置一个有意义的显示名称
hr = pSessionControl->SetDisplayName(L"我的应用 - 通话中", NULL);
if (FAILED(hr)) {
    // 处理错误
}

// 设置一个图标路径(可选,可以是可执行文件或DLL中的资源路径)
// 例如: "C:\\path\\to\\myapp.exe,-101" 表示myapp.exe中的图标资源ID 101
hr = pSessionControl->SetIconPath(L"%SystemRoot%\\System32\\mmres.dll,-3011", NULL); // 举例:用系统电话图标
if (FAILED(hr)) {
    // 处理错误
}

进阶技巧与思考:

  • IAudioSessionControl2 的作用: 尽管没有直接的 SetCategory,但 IAudioSessionControl2 接口提供了对会话状态(如静音、音量)的更精细控制,并且是系统管理现代音频会话的关键。确保你能查询到这个接口通常是好事。
    IAudioSessionControl2 *pSessionControl2 = nullptr;
    hr = pSessionControl->QueryInterface(__uuidof(IAudioSessionControl2), (void**)&pSessionControl2);
    if (SUCCEEDED(hr)) {
        // 获取成功,虽然我们不直接用它设置类别,但它的存在表明会话是可被现代策略管理的
        // 使用完后记得 pSessionControl2->Release();
    }
    
  • 流创建时的参数: 仔细检查 IAudioClient::Initialize 使用的 AUDCLNT_SHAREMODE(应为 AUDCLNT_SHAREMODE_SHARED,因为独占模式会静音其他应用)和 StreamFlags。虽然没有明确的“通信”标志位,但避免使用可能冲突的标志。
  • COM 初始化: 别忘了你的线程需要初始化 COM (CoInitializeEx(NULL, COINIT_APARTMENTTHREADED)COINIT_MULTITHREADED) 才能使用这些接口,并在结束时调用 CoUninitialize()。选择哪种线程模型取决于你的应用架构,但 UI 线程通常是 STA。音频处理线程可以是 MTA。

为什么之前的尝试可能没效果?

如果你仅仅是创建了一个标准的共享模式音频流,而没有做任何额外的配置,或者没有特意选择在“通信”音频设备上播放/录制,Windows 就没有足够的信息将你的音频流识别为通信活动。

安全建议:

  • 谨慎使用非公开 API,如 PolicyConfig 系列。它们可能在未来的 Windows 版本中发生变化或失效,导致你的应用出问题。尽可能坚持使用公开、有文档的 Core Audio API。
  • 正确管理 COM 对象生命周期。确保获取的每个接口指针都在不再需要时调用 Release(),防止资源泄漏。可以使用智能指针(如 Microsoft::WRL::ComPtr)来简化管理。
  • 做好错误处理。每个 COM 调用都返回 HRESULT,务必检查它是否 SUCCEEDED()FAILED(),并根据情况进行处理。

替代方案?WASAPI 独占模式回顾

再次强调,WASAPI 的独占模式(Exclusive Mode)不是解决这个问题的正确方法。

  • 原理: 独占模式允许一个应用程序完全控制音频设备,绕过 Windows 音频引擎的处理(包括音量混合、效果等)。
  • 效果: 当一个应用以独占模式使用音频设备时,所有其他应用的声音都会被完全静音
  • 为啥不适用: 这不符合用户在声音设置里选择的“降低音量”选项。用户可能只是希望背景音乐声音小一点,而不是完全消失。独占模式也跟触发那个特定的“检测到通信活动”设置无关。

所以,除非你的应用有特殊需求(比如专业音频编辑软件需要最低延迟和无干扰输出),否则应避免使用独占模式来实现音量闪避。

小结一下

想让 Windows 在你的应用进行通话时自动降低其他应用音量,关键在于“告知”系统你的音频流属于“通信”类别。虽然没有一个单一、明确的 SetAudioStreamCategory(Communications) API 调用,但通过组合以下策略可以有效地达到目的:

  1. 优先使用“默认通信设备” 来创建你的音频渲染/捕获流。通过 IMMDeviceEnumerator::GetDefaultAudioEndpoint 并指定 eCommunications 角色获取设备。
  2. 利用 IAudioSessionControl 为你的音频会话设置一个清晰表明“通信”意图的显示名称和图标。
  3. 确保使用的是共享模式 (AUDCLNT_SHAREMODE_SHARED) ,而不是独占模式。

通过这些方法,你的应用就能更好地与 Windows 的音频策略系统集成,让那个“声音”设置选项按预期工作。这需要对 Core Audio API 有一定的了解,并且细心处理 COM 对象的生命周期和错误情况。