Windows下Core Audio APIS 音频应用开发(一)

时间:2021-06-20 03:25:11
 http://msdn.microsoft.com/en-us/library/dd370784(v=vs.85).aspx文章翻译

译文转载自http://www.cppblog.com/shenhuafeng/archive/2006/12/12/16323.html

Core Audio APIS :

Vista 里面,一组新的用户态的音频组件提供给应用程序来改善应用程序操作音频的能力,

包括以下的一些方面:

低延时,几乎无故障的音频流。

提高可靠性 ( 很多音频函数从核心态移到了用户态 )

提高了安全性 (在安全的,低优先级别的线程处理被保护的音频内容)

分配了特定的系统级别的规则 (console, multimedia, communications) 给单独的音频设备。

用户可以直接操作,相应 endpoint 设备的软件抽象 ( 如:扩音器,耳麦及麦克风 ) 以下的高层 API 是以 Core Audio APIs 来工作的。

DirectSound

DirectMusic

Windows multimedia waveXxx and mixerXxx functions

Media Foundation

Streaming Audio Renderer (SAR)

绝大多数的音频应用程序与以上的高层次的 API 交互而不是直接操作底层的 Core Audio API 。例如以下一些应用可能用到高等级的 API :

        媒体播放器

        DVD 播放器

        游戏

        商用软件

通常这些应用用到 DirectSound 和媒体的底层函数。

通常的应用不需要直接用到 Core Audio API ,例如 Core Audio API 中的 Audio streams 需要使用一个音频设备的原始数据格式。然而,一些第三方的软件开发人员开发以下的产品时,需要用到这些核心的 API :

       专业的音频应用程序 (PRO AUDIO)

       实时通信 (RTC) 应用程序

       第三方音频 API

一个 PRO AUDIO 和 RTC 应用程序可能需要直接用底层 Core Audio API 访问音频硬件来达到最小延时的效果。一个第三方的音频 API 需要直接访问 Core Audio API 来实现高层的 API 没有提供的功能。

Core Audio API 包括:

        Multimedia Device (MMDevice) API :用这些 API 来枚举系统中的音频设备。

        Windows Audio Session API (WASAPI) :用这些 API 来创建和管理来自音频设备音频流。

        DeviceTopology API :用这些 API 来直接访问声音适配器中的硬件数据通路的拓扑特性(如音量控制,复用器等)

        EndpointVolume API :用这些 API 直接访问音频设备的声音控制。这些 API 通常是给那些以独占模式管理音频流的应用程序。

这些 API 提供对于设备的抽象概念,这些概念被描述成为 Audio Endpoint Device 。每个 API 包含很多 COM 接口。由于音频需要低延时和精确的同步,所 MMDevice, WASAPI, DeviceTopology, 和 EndpointVolume APIs 不依赖于 .NET 框架。

除了 Vista 之外其他的操作系统都不支持 Core Audio API 。包括: Microsoft Windows Server 2003, Windows XP, Windows ME, Windows 2000, 和 Windows 98 。

 

Vista 中的音频控制的角色概念:

假如系统中有多个音频设备,那么一个设备可能用户是用来播放电影的,另一个可能是用来玩游戏的。这样 Vista 中就引入了角色的概念。

ERole 常量

设备角色

渲染举例

捕获举例

eConsole

与计算机交互

游戏和系统的通告声音

语音命令

eCommunications

与他人的声音交流

 聊天和 VOIP

eMultimedia

播放或者录制电影和音乐

电影和音乐

实时的声音录制 

Vista 中的音量控制被分成 4 种级别: 

IAudioStream 接口提供 session 每个流的音量控制。

methods in the IAudioStreamVolume interface.

IChannelAudioVolume 接口提供 session 中每个通道的音量控制。

ISimpleAudioVolume 接口控制每一个 Session 的主音量。

假如需要更改设备的音量大小,则需要操作 IAudioEndpointVolume 接口。

开发举例: Vista 中控制系统音量

l         初始化 COM :

CoInitializeEx(NULL, COINIT_MULTITHREADED)

l         获取 IMMDeviceEnumerator 设备指针:

  CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL,

                     CLSCTX_ALL, __uuidof(IMMDeviceEnumerator),

                     (void**)&m_pEnumerator)

l         获取 IMMDevice 指针,这是是所有 MM 设备 — 多媒体设备的根

n         其中第一个参数是指明设备的用途

n         第二个参数指明设备角色

m_pEnumerator->GetDefaultAudioEndpoint(eRender, eMultimedia, &m_pDeviceOut)

l         获取 IAudioEndpointVolume 指针:(我们需要控制系统音量所对应的对象)

m_pDeviceOut->Activate(__uuidof(IAudioEndpointVolume),CLSCTX_ALL,NULL,(void**)&m_AudioEndpointVolume)

l         根据需要调用该对象的 API ,进行操作。

-----------------------------------------------------------------------------------------------------------------------------------

对于一个音频应用程序,最基本也是最重要的两个点就是:音频数据的采集;音频数据的播放。下面我们来看下如何用Core Audio APIS进行音频数据的采集。

    最权威的学习资料无疑是微软的MSDN官方资料,先给出链接地址,大家可以先去阅读下文档,虽然是英文,但挺好理解的,https://msdn.microsoft.com/en-us/library/dd370800(v=vs.85).aspx

    首先我们来了解下面几个概念:

    1. IMMDevice : 创建音频设备终端,我们可以把它简单的理解为设备对象

    2. IAudioClient : 创建一个用来管理音频数据流的对象,应用程序通过这个对象可以获取的音频设备里的数据,我们可以把它想象成一个大水池,里面都是一些数据

    3.  IAudioCaptureClient : 很明显,专用于获取采集数据的对象,它还有个兄弟IAudioRenderClient

    下面直接上官网示例代码,解释下代码大家就清楚怎么做了


   //首先枚举你的音频设备,你可以在这个时候获取到你机器上所有可用的设备,并指定你需要用到的那个设置
    hr = CoCreateInstance(
CLSID_MMDeviceEnumerator, NULL,
CLSCTX_ALL, IID_IMMDeviceEnumerator,
(void**)&pEnumerator);
EXIT_ON_ERROR(hr)

hr = pEnumerator->GetDefaultAudioEndpoint(
eCapture, eConsole, &pDevice);
EXIT_ON_ERROR(hr)

   // 创建一个管理对象,通过它可以获取到你需要的一切数据
hr = pDevice->Activate(
IID_IAudioClient, CLSCTX_ALL,
NULL, (void**)&pAudioClient);
EXIT_ON_ERROR(hr)

hr = pAudioClient->GetMixFormat(&pwfx);
EXIT_ON_ERROR(hr)

   //初始化管理对象,在这里,你可以指定它的最大缓冲区长度,这个很重要,应用程序控制数据块的大小以及延时长短都靠这里的初始化,具体参数大家看看文档解释
hr = pAudioClient->Initialize(
AUDCLNT_SHAREMODE_SHARED,
0,
hnsRequestedDuration,
0,
pwfx,
NULL);
EXIT_ON_ERROR(hr)

//这个buffersize,指的是缓冲区最多可以存放多少帧的数据量
hr = pAudioClient->GetBufferSize(&bufferFrameCount);
EXIT_ON_ERROR(hr)

    //创建采集管理接口,这个接口很简单,没什么重要的东西
hr = pAudioClient->GetService(
IID_IAudioCaptureClient,
(void**)&pCaptureClient);
EXIT_ON_ERROR(hr)

// Notify the audio sink which format to use.
hr = pMySink->SetFormat(pwfx);
EXIT_ON_ERROR(hr)

// Calculate the actual duration of the allocated buffer.
hnsActualDuration = (double)REFTIMES_PER_SEC *
bufferFrameCount / pwfx->nSamplesPerSec;

hr = pAudioClient->Start(); // Start recording.
EXIT_ON_ERROR(hr)

// Each loop fills about half of the shared buffer.
while (bDone == FALSE)
{
//让程序暂停运行一段时间,缓冲区里在这段时间会被填充数据
Sleep(hnsActualDuration/REFTIMES_PER_MILLISEC/2);

hr = pCaptureClient->GetNextPacketSize(&packetLength);
EXIT_ON_ERROR(hr)

while (packetLength != 0)
{
//锁定缓冲区,获取数据
hr = pCaptureClient->GetBuffer(
&pData,
&numFramesAvailable,
&flags, NULL, NULL);
EXIT_ON_ERROR(hr)

if (flags & AUDCLNT_BUFFERFLAGS_SILENT)
{
pData = NULL;
}

hr = pMySink->CopyData(
pData, numFramesAvailable, &bDone);
EXIT_ON_ERROR(hr)

hr = pCaptureClient->ReleaseBuffer(numFramesAvailable);
EXIT_ON_ERROR(hr)

hr = pCaptureClient->GetNextPacketSize(&packetLength);
EXIT_ON_ERROR(hr)
}
}

hr = pAudioClient->Stop();
EXIT_ON_ERROR(hr)

     代码就这么多,这是官网给出的最简单的一个关于获取音频数据的示例,这里面取到的数据是最原始的,我们可以对这些数据进去任何处理,达到我们需要的效果。

     对于初学者来说(比如笔者),文中的pMySink会让大家感到迷惑。大家都能猜到这个是自定义的一个类的对象,用来拷贝数据的,但里面做了一些什么,我们却不清楚。我这里多说一些(大牛请无视下面的话):

      音频数据量的计算:数据量(字节/秒)=
(采样频率(Hz)*采样位数(bit)*声道数)/ 8

我们回顾下代码,创建完管理对象后,我们就可以获取到当前设备的音频数据的格式,然后对pMySink设置了数据格式

hr
= pAudioClient->GetMixFormat(&pwfx);//这里面就可以获取的数据格式:频率,采集位数,声道数

 

hr = pMySink->SetFormat(pwfx);//指定数据格式,计算数据量

这就决定了,pMySink每一次读取数据量的大小,保证我们在获取的每一帧数据都是正确的

hr
= pMySink->CopyData(pData, numFramesAvailable, &bDone);

这里就是简单的拷贝数据了

---------------------------------------------------------------------------------------------------------------------------------

上篇我们将来如何运用Core Audio APIS进行声音数据的采集,下面我们来说说声音数据的播放。

    从第一篇文章我们知道,Core Audio APIS是Window下比较底层的API,使用它来播放声音的时候不像DirectSound这些高层的API那么方便,很多东西要自己来做。

    对于DX9以上的我不太了解,在DX9上,播放音频数据时,你只要设置好相应的数据格式,然后把数据往缓冲区里面塞就OK了;相比之下,Core Audio要复杂一些,下面我们来看看最理想化的一种情况,照例先给出官网地址 https://msdn.microsoft.com/en-us/library/dd316756(v=vs.85).aspx

    hr = CoCreateInstance(

              CLSID_MMDeviceEnumerator, NULL,
              CLSCTX_ALL, IID_IMMDeviceEnumerator,
              (void**)&pEnumerator);
    EXIT_ON_ERROR(hr)void**)&pAudioClient);
    EXIT_ON_ERROR(hr)


    hr = pAudioClient->GetMixFormat(&pwfx);
    EXIT_ON_ERROR(hr)


    hr = pAudioClient->Initialize(
                         AUDCLNT_SHAREMODE_SHARED,
                         0,
                         hnsRequestedDuration,
                         0,
                         pwfx,
                         NULL);
    EXIT_ON_ERROR(hr)


    // Tell the audio source which format to use.
    hr = pMySource->SetFormat(pwfx);
    EXIT_ON_ERROR(hr)


    // Get the actual size of the allocated buffer.
    hr = pAudioClient->GetBufferSize(&bufferFrameCount);
    EXIT_ON_ERROR(hr)


   
//注意,这里创建的是IAudioRenderClient,上一篇我们创建的是IAudioCaptureClient
    hr = pAudioClient->GetService(
                         IID_IAudioRenderClient,
                         (void**)&pRenderClient);
    EXIT_ON_ERROR(hr)


    // Grab the entire buffer for the initial fill operation.
    hr = pRenderClient->GetBuffer(bufferFrameCount, &pData);
    EXIT_ON_ERROR(hr)


    // Load the initial data into the shared buffer.
    hr = pMySource->LoadData(bufferFrameCount, pData, &flags);
    EXIT_ON_ERROR(hr)


    hr = pRenderClient->ReleaseBuffer(bufferFrameCount, flags);
    EXIT_ON_ERROR(hr)


    // Calculate the actual duration of the allocated buffer.
    hnsActualDuration = (double)REFTIMES_PER_SEC *
                        bufferFrameCount / pwfx->nSamplesPerSec;


    hr = pAudioClient->Start();  // Start playing.
    EXIT_ON_ERROR(hr)


    //上面那些之前解释过,并没有什么太大的区别,着重来看下这个循环的代码
    // Each loop fills about half of the shared buffer.
    while (flags != AUDCLNT_BUFFERFLAGS_SILENT)
    {
        // Sleep for half the buffer duration.
        Sleep((DWORD)(hnsActualDuration/REFTIMES_PER_MILLISEC/2));


        //获取缓冲区内还未播放完的数据帧数
        hr = pAudioClient->GetCurrentPadding(&numFramesPadding);
        EXIT_ON_ERROR(hr)


        //获取空余的缓冲区长度,之前说过,缓冲区有个最大的长度,这个你可以设置的
        numFramesAvailable = bufferFrameCount - numFramesPadding;


        // 写入数据
        hr = pRenderClient->GetBuffer(numFramesAvailable, &pData);
        EXIT_ON_ERROR(hr)


        // Get next 1/2-second of data from the audio source.
        hr = pMySource->LoadData(numFramesAvailable, pData, &flags);
        EXIT_ON_ERROR(hr)


        hr = pRenderClient->ReleaseBuffer(numFramesAvailable, flags);
        EXIT_ON_ERROR(hr)
    }


    // Wait for last data in buffer to play before stopping.
    Sleep((DWORD)(hnsActualDuration/REFTIMES_PER_MILLISEC/2));


    hr = pAudioClient->Stop();  // Stop playing.

   

   上面是最理想也是最简单的情况。何为最理想,就是你想要播放的音频数据,它的数据格式刚好与播放设备所支持的格式是一致的,这样这里就不涉及到数据的重抽样。也许这样说的有点模糊,下面我们来举个例子。

    假如你设备所支持的格式是:采集频率16000HZ;而你的数据的采集频率是8000HZ.(其它参数我们默认是一样的,简单一些)。如果我们什么都不做,直接把数据写入到缓冲区,会有什么效果?

     没错,我们可以猜测到,声音的播放速度快了一倍,为什么?根据数据量的计算,我们输入的数据量少了一倍,本来我们输入2秒的数据量,结果设备只用了一秒就播放完了,这样播放的效果自然是不行的,为了达到理想的效果,我们必须对输入的数据进行一次重抽样,经过重抽样后的数据才符合设备的要求


-------------------------------------------------------------------------------------------------------------------------------

前面说了音频数据的采集和播放,接下来我们说说如何利用Core Audio APIS来控制音量。

    从官网文档的编排上看,在介绍Volume Controls之前,文档上先给我们介绍了一个比较重要的概念:Audio Sessions!大家可以去看看,我觉得还是有必要了解下的(我也还没有理解透彻,就不误导大家了)。

     下面看看Core Audio API关于音量控制这方面的函数吧,主要有:

     1. ISimpleAudioVolume

     2. IChannelAudioVolume

     3.IAudioStreamVolume

     4.IAudioEndpointVolume 

  其中,前面三种只适用于共享模式,第四种可用于独占模式,也可用于共享模式。前面三种Api的用法很相似,在此只介绍IChannelAudioVolume的用法,下面直接上代码:

     

    hr = CoCreateInstance(
CLSID_MMDeviceEnumerator, NULL,
CLSCTX_ALL, IID_IMMDeviceEnumerator,
(void**)&pEnumerator);
EXIT_ON_ERROR(hr)

hr = pEnumerator->GetDefaultAudioEndpoint(
eCapture, eConsole, &pDevice);
EXIT_ON_ERROR(hr);
    hr = pDevice->Activate(
IID_IAudioClient, CLSCTX_ALL,
NULL, (void**)&pAudioClient);
EXIT_ON_ERROR(hr)

hr = pAudioClient->GetMixFormat(&pwfx);
EXIT_ON_ERROR(hr)
    hr = pAudioClient->Initialize(
AUDCLNT_SHAREMODE_SHARED,
0,
hnsRequestedDuration,
0,
pwfx,
NULL);
EXIT_ON_ERROR(hr)

hr = pAudioClient->GetBufferSize(&bufferFrameCount);
EXIT_ON_ERROR(hr)
    hr = pAudioClient->GetService(
IID_IAudioCaptureClient,
(void**)&pCaptureClient);


       //前面的代码不解释,下面是创建一个音量控制的指针,获取接口,设置音量,很简单吧

        IChannelAudioVolume *   _IChannelAudioVolume;
hr = pAudioClient->GetService(IID_IChannelAudioVolume,(void**)&_IChannelAudioVolume);
float volume[2] = {1.0,1.0};
_IChannelAudioVolume->SetAllVolumes(2,volume,NULL);


     关于IAUDIOEndpointVolume的用法,略显复杂,不过官网给出了详细的参考代码,大家可自行去查看,https://msdn.microsoft.com/en-us/library/dd370839(v=vs.85).aspx

    具体思路是:

     1. 自定义一个回调类(从IAudioEndpointVolumeCallback 派生,重写相应的一些函数)

     2. 定义IAudioEndpointVolume指针,并获取相应的接口

     3. 注册回调(主要是为了客户端程序获悉音量发生变化时,能做出相应的处理,比如调解音量滑动条等等)