Share via


轉譯數據流

用戶端會呼叫 IAudioRenderClient 介面中的 方法,以將轉譯數據寫入端點緩衝區。 針對共用模式數據流,用戶端會與音訊引擎共用端點緩衝區。 針對獨佔模式數據流,用戶端會與音訊裝置共用端點緩衝區。 若要要求特定大小的端點緩衝區,用戶端會呼叫 IAudioClient::Initialize 方法。 若要取得配置緩衝區的大小,這可能與要求的大小不同,用戶端會呼叫 IAudioClient::GetBufferSize 方法。

若要透過端點緩衝區移動轉譯數據的數據流,用戶端會替代呼叫 IAudioRenderClient::GetBuffer 方法和 IAudioRenderClient::ReleaseBuffer 方法。 用戶端會將端點緩衝區中的數據當做一系列數據封包來存取。 GetBuffer 呼叫會擷取下一個封包,讓用戶端可以填入轉譯數據。 將數據寫入封包之後,用戶端會呼叫 ReleaseBuffer ,將已完成的封包新增至轉譯佇列。

對於轉譯緩衝區,IAudioClient::GetCurrentPadding 方法所報告的填補值代表排入佇列以在緩衝區中播放的轉譯數據量。 轉譯應用程式可以使用填補值來判斷它可以安全地寫入緩衝區的新數據量,而不需要覆寫音訊引擎尚未從緩衝區讀取先前寫入的數據的風險。 可用空間只是緩衝區大小減去填補大小。 用戶端可以要求封包大小,代表下一個 GetBuffer 呼叫中的部分或所有可用空間。

封包的大小會以 音訊畫面表示。 PCM 數據流中的音訊畫面是一組樣本(該組包含數據流中每個通道的一個樣本),同時播放或錄製(時鐘刻度)。 因此,音訊畫面的大小是樣本大小乘以數據流中的通道數目。 例如,具有16位樣本的立體聲 (2 通道) 資料流的畫面大小是四個字節。

下列程式代碼範例示範如何在預設轉譯裝置上播放音訊數據流:

//-----------------------------------------------------------
// 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 flagsflags變數會將此值傳達給 IAudioRenderClient::ReleaseBuffer 方法,此方法會以無聲填滿緩衝區中的指定畫面格數目來回應。

在對 IAudioClient::Initialize 方法的呼叫中,上述範例中的 PlayAudioStream 函式會要求具有一秒持續時間的共用緩衝區。 (配置緩衝區的持續時間可能稍長。在對 IAudioRenderClient::GetBuffer 和 IAudioRenderClient::ReleaseBuffer 方法的初始呼叫中,函式會在呼叫 IAudioClient::Start 方法開始播放緩衝區之前填滿整個緩衝區。

在main迴圈中,函式會反覆填滿緩衝區的一半,間隔為半秒。 就在主要迴圈中 Windows Sleep 函式的每個呼叫之前,緩衝區已滿或幾乎已滿。 當睡眠呼叫傳回時,緩衝區大約已滿一半。 迴圈會在 LoadData 函式的最終呼叫之後結束,將 flags 變數設定為值AUDCLNT_BUFFERFLAGS_SILENT。 此時,緩衝區至少包含一個實際數據框架,而且它可能包含多達半秒的實際數據。 緩衝區的其餘部分包含無聲。 迴圈後面的睡眠呼叫提供足夠的時間(半秒)來播放所有剩餘的數據。 在呼叫 IAudioClient::Stop 方法之前,數據之後的無聲會防止不必要的音效停止音訊串流。 如需睡眠的詳細資訊,請參閱 Windows SDK 檔。

在呼叫 IAudioClient::Initialize 方法之後,數據流會保持開啟狀態,直到客戶端釋放其 IAudioClient 介面的所有參考,以及用戶端透過 IAudioClient::GetService 方法取得之服務介面的所有參考為止。 最終 的 Release 呼叫會關閉數據流。

上述程式代碼範例中的 PlayAudioStream 函式會呼叫 CoCreateInstance 函式,為系統中的音頻端點裝置建立列舉值。 除非先前呼叫 CoCreateInstance CoInitializeEx 函式來初始化 COM 連結庫的呼叫程式,否則 CoCreateInstance 呼叫將會失敗。 如需 CoCreateInstance、CoCreateInstanceCoInitializeEx 的詳細資訊,請參閱 Windows SDK 檔。

串流管理