스트림 렌더링

클라이언트는 IAudioRenderClient 인터페이스의 메서드를 호출하여 렌더링 데이터를 엔드포인트 버퍼에 씁니다. 공유 모드 스트림의 경우 클라이언트는 엔드포인트 버퍼를 오디오 엔진과 공유합니다. 단독 모드 스트림의 경우 클라이언트는 엔드포인트 버퍼를 오디오 디바이스와 공유합니다. 특정 크기의 엔드포인트 버퍼를 요청하기 위해 클라이언트는 IAudioClient::Initialize 메서드를 호출합니다 . 요청된 크기와 다를 수 있는 할당된 버퍼의 크기를 가져오기 위해 클라이언트는 IAudioClient::GetBufferSize 메서드를 호출합니다.

엔드포인트 버퍼를 통해 렌더링 데이터 스트림을 이동하려면 클라이언트가 IAudioRenderClient::GetBuffer 메서드 및 IAudioRenderClient::ReleaseBuffer 메서드를 호출합니다. 클라이언트는 일련의 데이터 패킷으로 엔드포인트 버퍼의 데이터에 액세스합니다. GetBuffer 호출은 클라이언트가 렌더링 데이터로 채울 수 있도록 다음 패킷을 검색합니다. 패킷에 데이터를 쓴 후 클라이언트는 ReleaseBuffer 를 호출하여 완성된 패킷을 렌더링 큐에 추가합니다.

렌더링 버퍼의 경우 IAudioClient::GetCurrentPadding 메서드에서 보고하는 패딩 값은 버퍼에서 재생하기 위해 대기 중인 렌더링 데이터의 양을 나타냅니다. 렌더링 애플리케이션은 패딩 값을 사용하여 오디오 엔진이 버퍼에서 아직 읽지 않은 이전에 작성된 데이터를 덮어쓸 위험 없이 버퍼에 안전하게 쓸 수 있는 새 데이터의 양을 결정할 수 있습니다. 사용 가능한 공간은 단순히 버퍼 크기에서 패딩 크기를 뺀 값입니다. 클라이언트는 다음 GetBuffer 호출에서 사용 가능한 공간의 일부 또는 전부를 나타내는 패킷 크기를 요청할 수 있습니다.

패킷의 크기는 오디오 프레임으로 표현됩니다. PCM 스트림의 오디오 프레임은 재생되거나 동시에 기록되는 샘플 집합(스트림의 각 채널에 대해 하나의 샘플 포함)입니다(클록 틱). 따라서 오디오 프레임의 크기는 스트림의 채널 수를 곱한 샘플 크기입니다. 예를 들어 16비트 샘플이 있는 스테레오(2 채널) 스트림의 프레임 크기는 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 함수는 클라이언트 정의 클래스인 MyAudioSource에 속하는 개체에 대한 포인터인 단일 매개 변수 pMySource를 사용합니다. 두 멤버 함수인 LoadData 및 SetFormat이 있습니다. 예제 코드에는 다음과 같은 이유로 MyAudioSource 구현이 포함되지 않습니다.

  • 클래스 멤버 중 어느 것도 WASAPI의 인터페이스에 있는 메서드와 직접 통신하지 않습니다.
  • 클래스는 클라이언트의 요구 사항에 따라 다양한 방법으로 구현할 수 있습니다. 예를 들어 WAV 파일에서 렌더링 데이터를 읽고 스트림 형식으로 즉석 변환을 수행할 수 있습니다.

그러나 두 함수의 작업에 대한 일부 정보는 예제를 이해하는 데 유용합니다.

LoadData 함수는 지정된 수의 오디오 프레임(첫 번째 매개 변수)을 지정된 버퍼 위치(두 번째 매개 변수)에 씁니다. (오디오 프레임의 크기는 스트림의 채널 수에 샘플 크기를 곱한 수입니다.) PlayAudioStream 함수는 LoadData를 사용하여 공유 버퍼의 일부를 오디오 데이터로 채웁니다. SetFormat 함수는 데이터에 사용할 LoadData 함수의 형식을 지정합니다. LoadData 함수가 지정된 버퍼 위치에 하나 이상의 프레임을 쓸 수 있지만 지정된 수의 프레임을 작성하기 전에 데이터가 부족하면 나머지 프레임에 무음이 기록됩니다.

LoadData가 지정된 버퍼 위치에 하나 이상의 실제 데이터 프레임(무음이 아님)을 작성하는 데 성공하는 한, 위의 코드 예제에서 변수에 대한 출력 포인터 flags 인 세 번째 매개 변수를 통해 0을 출력합니다. LoadData가 데이터가 부족하여 지정된 버퍼 위치에 단일 프레임도 쓸 수 없는 경우 버퍼에 아무 것도 쓰지 않고(침묵하지 않음) 변수에 AUDCLNT_BUFFERFLAGS_SILENT flags 값을 씁니다. 변수는 flags 이 값을 IAudioRenderClient::ReleaseBuffer 메서드에 전달합니다. 이 메서드는 버퍼의 지정된 프레임 수를 무음으로 채워 응답합니다.

IAudioClient::Initialize 메서드에 대한 호출에서 이전 예제의 PlayAudioStream 함수는 지속 시간이 1초인 공유 버퍼를 요청합니다. (할당된 버퍼의 기간이 약간 더 길 수 있습니다.) IAudioRenderClient::GetBufferIAudioRenderClient::ReleaseBuffer 메서드에 대한 초기 호출에서 함수는 IAudioClient::Start 메서드를 호출하여 버퍼 재생을 시작하기 전에 전체 버퍼를 채웁니다.

기본 루프 내에서 함수는 버퍼의 절반을 반초 간격으로 반복적으로 채웁니다. 기본 루프에서 Windows Sleep 함수를 호출하기 직전에 버퍼가 가득 차거나 거의 가득 찼습니다. 절전 모드 호출이 반환되면 버퍼가 절반 정도 가득 찼습니다. LoadData 함수에 대한 최종 호출 후 루프가 종료되어 변수가 flags AUDCLNT_BUFFERFLAGS_SILENT 값으로 설정됩니다. 이 시점에서 버퍼에는 하나 이상의 실제 데이터 프레임이 포함되며 실제 데이터의 반초 정도가 포함될 수 있습니다. 버퍼의 나머지 부분에는 무음이 포함됩니다. 루프를 따르는 절전 모드 호출은 나머지 데이터를 모두 재생할 수 있는 충분한 시간(반초)을 제공합니다. 데이터를 따르는 무음은 IAudioClient::Stop 메서드 호출이 오디오 스트림을 중지하기 전에 원치 않는 소리를 방지합니다. 절전 모드에 대한 자세한 내용은 Windows SDK 설명서를 참조하세요.

IAudioClient::Initialize 메서드를 호출한 후 클라이언트가 IAudioClient 인터페이스에 대한 모든 참조와 클라이언트가 IAudioClient::GetService 메서드를 통해 얻은 서비스 인터페이스에 대한 모든 참조를 해제할 때까지 스트림은 열린 상태로 유지됩니다. 최종 릴리스 호출은 스트림을 닫습니다.

이전 코드 예제의 PlayAudioStream 함수는 CoCreateInstance 함수를 호출하여 시스템의 오디오 엔드포인트 디바이스에 대한 열거자를 만듭니다. 이전에 COM 라이브러리를 초기화하기 위해 CoCreateInstance 또는 CoInitializeEx 함수를 호출한 호출 프로그램이 아니면 CoCreateInstance 호출이 실패합니다. CoCreateInstance, CoCreateInstanceCoInitializeEx에 대한 자세한 내용은 Windows SDK 설명서를 참조하세요.

스트림 관리