传统音频应用程序的音频事件

传统音频 API(如 DirectSound、DirectShow 和 waveOutXxx 函数)让应用程序能够获取和设置音频流的音量水平。 应用程序可以使用这些 API 中的音量控件功能,在应用程序窗口中显示音量滑块。

在 Windows Vista 中,系统音量控制程序 Sndvol 让用户能够控制各个应用程序的音频音量水平。 应用程序显示的音量滑块应与 Sndvol 中相应的音量滑块相链接。 如果用户通过应用程序窗口中的音量滑块来调整应用程序音量,则 Sndvol 中相应的音量滑块会立即移动以显示新的音量水平。 反之,如果用户通过 Sndvol 来调整应用程序音量,则应用程序窗口中的音量滑块应移动以显示新的音量水平。

在 Windows Vista 中,Sndvol 会立即反映应用程序通过调用 IDirectSoundBuffer::SetVolume 方法或 waveOutSetVolume 函数所做出的音量更改。 但是,DirectSound 或 waveOutXxx 函数等传统音频 API 无法在用户通过 Sndvol 更改应用程序音量时通知应用程序。 如果应用程序显示音量滑块,但没有收到音量变化通知,那么滑块将无法反映用户在 Sndvol 中进行的更改。 要实施适当的行为,应用程序设计人员必须以某种方式来弥补传统音频 API 通知不足的问题。

一种解决方案是,应用程序可以设置一个定时器,以便定期提醒自己检查音量水平是否发生变化。

更完美的解决方案是让应用程序使用核心音频 API 的事件通知功能。 具体而言,应用程序可以注册一个 IAudioSessionEvents 接口,以便在音量发生变化或发生其他类型的音频事件时接收回调。 当音量发生变化时,音量变化回调例程就能立即更新应用程序的音量滑块,从而反映相关的变化。

以下代码示例展示了应用程序如何注册以接收音量变化以及其他音频事件的通知:

//-----------------------------------------------------------
// Register the application to receive notifications when the
// volume level changes on the default process-specific audio
// session (with session GUID value GUID_NULL) on the audio
// endpoint device with the specified data-flow direction
// (eRender or eCapture) and device role.
//-----------------------------------------------------------
#define EXIT_ON_ERROR(hr)  \
              if (FAILED(hr)) { goto Exit; }
#define SAFE_RELEASE(punk)  \
              if ((punk) != NULL)  \
                { (punk)->Release(); (punk) = NULL; }

class AudioVolumeEvents
{
    HRESULT _hrStatus;
    IAudioSessionManager *_pManager;
    IAudioSessionControl *_pControl;
    IAudioSessionEvents *_pAudioEvents;
public:
    AudioVolumeEvents(EDataFlow, ERole, IAudioSessionEvents*);
    ~AudioVolumeEvents();
    HRESULT GetStatus() { return _hrStatus; };
};

// Constructor
AudioVolumeEvents::AudioVolumeEvents(EDataFlow flow, ERole role,
                                     IAudioSessionEvents *pAudioEvents)
{
    IMMDeviceEnumerator *pEnumerator = NULL;
    IMMDevice *pDevice = NULL;

    _hrStatus = S_OK;
    _pManager = NULL;
    _pControl = NULL;
    _pAudioEvents = pAudioEvents;

    if (_pAudioEvents == NULL)
    {
        _hrStatus = E_POINTER;
        return;
    }

    _pAudioEvents->AddRef();

    // Get the enumerator for the audio endpoint devices
    // on this system.
    _hrStatus = CoCreateInstance(__uuidof(MMDeviceEnumerator),
                                 NULL, CLSCTX_INPROC_SERVER,
                                 __uuidof(IMMDeviceEnumerator),
                                 (void**)&pEnumerator);
    EXIT_ON_ERROR(_hrStatus)

    // Get the audio endpoint device with the specified data-flow
    // direction (eRender or eCapture) and device role.
    _hrStatus = pEnumerator->GetDefaultAudioEndpoint(flow, role,
                                                     &pDevice);
    EXIT_ON_ERROR(_hrStatus)

    // Get the session manager for the endpoint device.
    _hrStatus = pDevice->Activate(__uuidof(IAudioSessionManager),
                                  CLSCTX_INPROC_SERVER, NULL,
                                  (void**)&_pManager);
    EXIT_ON_ERROR(_hrStatus)

    // Get the control interface for the process-specific audio
    // session with session GUID = GUID_NULL. This is the session
    // that an audio stream for a DirectSound, DirectShow, waveOut,
    // or PlaySound application stream belongs to by default.
    _hrStatus = _pManager->GetAudioSessionControl(NULL, 0, &_pControl);
    EXIT_ON_ERROR(_hrStatus)

    _hrStatus = _pControl->RegisterAudioSessionNotification(_pAudioEvents);
    EXIT_ON_ERROR(_hrStatus)

Exit:
    SAFE_RELEASE(pEnumerator)
    SAFE_RELEASE(pDevice)
}

// Destructor
AudioVolumeEvents::~AudioVolumeEvents()
{
    if (_pControl != NULL)
    {
        _pControl->UnregisterAudioSessionNotification(_pAudioEvents);
    }
    SAFE_RELEASE(_pManager)
    SAFE_RELEASE(_pControl)
    SAFE_RELEASE(_pAudioEvents)
};

前面的代码示例实现了一个名为 AudioVolumeEvents 的类。 在程序初始化过程中,音频应用程序会通过创建 AudioVolumeEvents 对象来启用音频事件通知。 此类的构造函数采用三个输入参数:

构造函数会将流和角色值作为输入参数提供给 IMMDeviceEnumerator::GetDefaultAudioEndpoint 方法。 该方法会创建一个 IMMDevice 对象,在其中封装了具有指定数据流方向和设备角色的音频终结点设备。

应用程序实现了 pAudioEvents 所指向的对象。 (前面的代码示例中未显示实现方法。有关实现 IAudioSessionEvents 接口的代码示例,请参阅音频会话事件)。此接口中的每种方法都会接收特定类型音频事件的通知。 如果应用程序对某一特定事件类型不感兴趣,那么该事件类型的方法除了返回 S_OK 以外,什么也不应执行。

IAudioSessionEvents::OnSimpleVolumeChanged 方法会接收音量变化通知。 通常,这种方法会更新应用程序的音量滑块。

在前面的代码示例中,AudioVolumeEvents 类的构造函数注册特定于进程的音频会话的通知,而该会话由会话 GUID 值 GUID_NULL 标识。 默认情况下,传统音频 API(如 DirectSound、DirectShow 和 waveOutXxx 函数)会将其流分配给此会话。 但是,DirectSound 或 DirectShow 应用程序可以选择覆盖默认行为,并将其流分配给跨进程会话或由 GUID 值(但 GUID_NULL 除外)标识的会话。 (目前还没有为 waveOutXxx 应用程序提供以类似方式覆盖默认行为的机制。)有关使用此行为的 DirectShow 应用程序的代码示例,请参阅DirectShow 应用程序的设备角色。 为了适应这种应用,可以修改前面代码示例中的构造函数,使其接受两个额外的输入参数 — 会话 GUID 和一个标志,以指示要监控的会话是跨进程会话还是特定进程会话。 将这些参数传递给构造函数中的 IAudioSessionManager::GetAudioSessionControl 方法调用。

在构造函数调用 IAudioSessionControl::RegisterAudioSessionNotification 方法注册通知后,只要 IAudioSessionControlIAudioSessionManager 接口存在,应用程序就会继续接收通知。 上述代码示例中的 AudioVolumeEvents 对象在调用析构函数之前,会一直保留着对这些接口的引用。 此行为可确保应用程序在 AudioVolumeEvents 对象的生命周期内持续接收通知。

DirectSound 或传统 Windows 多媒体应用程序可能会允许用户从可用设备列表中明确选择设备,而不是根据设备角色来隐式选择音频设备。 要支持这种行为,必须修改前面的代码示例,以便为所选设备生成音频事件通知。 需要进行两项修改。 首先,更改构造函数定义,以接受终结点 ID 字符串作为输入参数(代替代码示例中的流程和角色参数)。 此字符串用于标识与所选 DirectSound 或传统波形设备相对应的音频终结点设备。 其次,将调用 IMMDeviceEnumerator::GetDefaultAudioEndpoint 方法改为调用 IMMDeviceEnumerator::GetDevice 方法。 GetDevice 调用会将终结点 ID 字符串作为输入参数,并创建一个由该字符串标识的终结点设备实例。

获取 DirectSound 设备或传统波形设备终结点 ID 字符串的方法如下所示。

首先,在设备枚举过程中,DirectSound 会为每个枚举的设备提供终结点 ID 字符串。 要开始设备枚举,应用程序会将回调函数指针作为输入参数传递给 DirectSoundCreateDirectSoundCaptureCreate 函数。 回调函数的定义是:

BOOL DSEnumCallback(
  LPGUID  lpGuid,
  LPCSTR  lpcstrDescription,
  LPCSTR  lpcstrModule,
  LPVOID  lpContext
);

在 Windows Vista 中,lpcstrModule 参数会指向终结点 ID 字符串。 (在包括 Windows Server 2003、Windows XP 和 Windows 2000 在内的 Windows 早期版本中,lpcstrModule 参数会指向设备驱动程序模块的名称。)lpcstrDescription 参数会指向一个包含设备友好名称的字符串。 有关 DirectSound 设备枚举的详细信息,请参阅 Windows SDK 文档。

其次,要获取传统波形设备的终结点 ID 字符串,可使用 waveOutMessagewaveInMessage 函数向波形设备驱动程序发送 DRV_QUERYFUNCTIONINSTANCEID 消息。 有关使用此信息的代码示例,请参阅传统 Windows 多媒体应用程序的设备角色

与传统音频 API 的互操作性