呈现流

客户端调用 IAudioRenderClient 接口中的方法,以将呈现数据写入终结点缓冲区。 对于共享模式流,客户端与音频引擎共享终结点缓冲区。 对于独占模式流,客户端与音频设备共享终结点缓冲区。 要请求特定大小的终结点缓冲区,客户端会调用 IAudioClient::Initialize 方法。 要获取已分配缓冲区的大小(可能与请求的大小不同),客户端会调用 IAudioClient::GetBufferSize 方法。

若要通过终结点缓冲区移动呈现数据流,客户端会交替调用 IAudioRenderClient::GetBuffer 方法和 IAudioRenderClient::ReleaseBuffer 方法。 客户端会以一系列数据包的形式访问终结点缓冲区中的数据。 GetBuffer 调用将检索下一个数据包,以便客户端可以使用呈现数据进行填充。 将数据写入数据包后,客户端会调用 ReleaseBuffer 将完成的数据包添加到呈现队列。

对于呈现缓冲区,IAudioClient::GetCurrentPadding 方法报告的填充值表示在缓冲区中排队播放的呈现数据量。 呈现应用程序可以使用填充值来确定可以安全地写入缓冲区的新数据量,而不存在覆盖音频引擎尚未从缓冲区读取的以前写入的数据的风险。 可用空间只是缓冲区大小减去填充大小。 客户端可以在下一次 GetBuffer 调用中请求表示部分或全部可用空间的数据包大小。

数据包的大小以音频帧表示。 PCM 流中的音频帧是一个同时(时钟计时周期)播放或录制的样本集(该集包含流中每个通道的一个样本)。 因此,音频帧的大小是样本大小乘以流中的通道数。 例如,具有 16 位样本的立体声(双通道)流的帧大小为 4 字节。

下面的代码示例显示如何在默认呈现设备上播放音频流:

//-----------------------------------------------------------
// Play an audio stream on the default audio rendering
// device. The PlayAudioStream function allocates a shared
// buffer big enough to hold one second of PCM audio data.
// The function uses this buffer to stream data to the
// rendering device. The inner loop runs every 1/2 second.
//-----------------------------------------------------------

// REFERENCE_TIME time units per second and per millisecond
#define REFTIMES_PER_SEC  10000000
#define REFTIMES_PER_MILLISEC  10000

#define EXIT_ON_ERROR(hres)  \
              if (FAILED(hres)) { goto Exit; }
#define SAFE_RELEASE(punk)  \
              if ((punk) != NULL)  \
                { (punk)->Release(); (punk) = NULL; }

const CLSID CLSID_MMDeviceEnumerator = __uuidof(MMDeviceEnumerator);
const IID IID_IMMDeviceEnumerator = __uuidof(IMMDeviceEnumerator);
const IID IID_IAudioClient = __uuidof(IAudioClient);
const IID IID_IAudioRenderClient = __uuidof(IAudioRenderClient);

HRESULT PlayAudioStream(MyAudioSource *pMySource)
{
    HRESULT hr;
    REFERENCE_TIME hnsRequestedDuration = REFTIMES_PER_SEC;
    REFERENCE_TIME hnsActualDuration;
    IMMDeviceEnumerator *pEnumerator = NULL;
    IMMDevice *pDevice = NULL;
    IAudioClient *pAudioClient = NULL;
    IAudioRenderClient *pRenderClient = NULL;
    WAVEFORMATEX *pwfx = NULL;
    UINT32 bufferFrameCount;
    UINT32 numFramesAvailable;
    UINT32 numFramesPadding;
    BYTE *pData;
    DWORD flags = 0;

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

    hr = pEnumerator->GetDefaultAudioEndpoint(
                        eRender, 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)

    // 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)

    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));

        // See how much buffer space is available.
        hr = pAudioClient->GetCurrentPadding(&numFramesPadding);
        EXIT_ON_ERROR(hr)

        numFramesAvailable = bufferFrameCount - numFramesPadding;

        // Grab all the available space in the shared buffer.
        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.
    EXIT_ON_ERROR(hr)

Exit:
    CoTaskMemFree(pwfx);
    SAFE_RELEASE(pEnumerator)
    SAFE_RELEASE(pDevice)
    SAFE_RELEASE(pAudioClient)
    SAFE_RELEASE(pRenderClient)

    return hr;
}

在上例中,PlayAudioStream 函数接收一个参数 pMySource,该参数是指向属于客户端定义的类 MyAudioSource 的对象的指针,而该类包含两个成员函数:LoadData 和 SetFormat。 示例代码中不包括 MyAudioSource 的实现,这是因为:

  • 没有一个类成员可直接与 WASAPI 接口中的任何方法通信。
  • 该类能以多种方式实现,具体取决于客户端的要求。 (例如,它可以从 WAV 文件读取呈现数据,并执行流格式的实时转换。)

但是,有关这两个函数操作的一些信息对于理解示例还是很有用的。

LoadData 函数将指定数量的音频帧(第一个参数)写入指定的缓冲区位置(第二个参数)。 (音频帧的大小是流中的通道数乘以样本大小。)PlayAudioStream 函数使用 LoadData 为部分共享缓冲区填充音频数据。 SetFormat 函数指定 LoadData 函数要使用的数据格式。 如果 LoadData 函数能够将至少一个帧写入指定的缓冲区位置,但在写入指定帧数之前数据不足,则将静默写入剩余帧。

只要 LoadData 成功将至少一帧真实数据(不静默)写入指定的缓冲区位置,就会输出 0 到第三个参数,在前面的代码示例中是指向 flags 变量的输出指针。 当 LoadData 数据不足,甚至连单个帧也无法写入指定的缓冲区位置时,将不向缓冲区(即使静默也不)写入任何内容,而是将值 AUDCLNT_BUFFERFLAGS_SILENT 写入 flags 变量。 flags 变量将此值传达给 IAudioRenderClient::ReleaseBuffer 方法,从而通过在静默状态下在缓冲区中填充指定的帧数来做出响应。

在对 IAudioClient::Initialize 方法的调用中,前面示例中的 PlayAudioStream 函数请求一个持续时间为 1 秒的共享缓冲区。 (分配的缓冲区持续时间可能略长。)在对 IAudioRenderClient::GetBufferIAudioRenderClient::ReleaseBuffer 方法的初始调用中,该函数在调用 IAudioClient::Start 方法开始播放缓冲区之前填充整个缓冲区。

在主循环中,该函数通过迭代方式以半秒间隔填充缓冲区的一半。 每次在主循环中调用 Windows Sleep 函数之前,缓冲区均为已满或几乎已满。 当 Sleep 调用返回时,缓冲区大约为半满。 在对 LoadData 函数的最终调用后结束的循环将 flags 变量设置为值 AUDCLNT_BUFFERFLAGS_SILENT。 此时,缓冲区至少包含一帧真实数据,并且可能包含多达半秒的真实数据。 缓冲区的其余部分包含静音。 循环后的 Sleep 调用提供足够的时间(半秒)来播放所有剩余数据。 在 IAudioClient::Stop 方法调用停止音频流之前,数据后面的静音会阻止不必要的声音。 有关 Sleep 的详细信息,请参阅 Windows SDK 文档。

调用 IAudioClient::Initialize 方法后,数据流将保持打开状态,直到客户端释放对 IAudioClient 接口的所有引用,以及对客户端通过 IAudioClient::GetService 方法获得的服务接口的所有引用。 最后的 Release 调用将关闭流。

前面代码示例中的 PlayAudioStream 函数会调用 CoCreateInstance 函数,为系统中的音频终结点设备创建一个枚举器。 除非调用程序先前调用了 CoCreateInstanceCoInitializeEx 函数来初始化 COM 库,否则 CoCreateInstance 调用将失败。 有关 CoCreateInstanceCoCreateInstanceCoInitializeEx 的详细信息,请参阅 Windows SDK 文档。

流管理