Share via


Exclusive-Mode 스트림

앞에서 설명한 것처럼 애플리케이션이 단독 모드로 스트림을 열면 애플리케이션은 스트림을 재생하거나 기록하는 오디오 엔드포인트 디바이스를 단독으로 사용합니다. 반면, 여러 애플리케이션은 디바이스에서 공유 모드 스트림을 열어 오디오 엔드포인트 디바이스를 공유할 수 있습니다.

오디오 디바이스에 대한 단독 모드 액세스는 중요한 시스템 소리를 차단하고, 다른 애플리케이션과의 상호 운용성을 방지하고, 그렇지 않으면 사용자 환경을 저하시킬 수 있습니다. 이러한 문제를 완화하기 위해 전용 모드 스트림이 있는 애플리케이션은 일반적으로 애플리케이션이 포그라운드 프로세스가 아니거나 적극적으로 스트리밍되지 않을 때 오디오 디바이스의 제어를 포기합니다.

스트림 대기 시간은 애플리케이션의 엔드포인트 버퍼를 오디오 엔드포인트 디바이스와 연결하는 데이터 경로에 내재된 지연입니다. 렌더링 스트림의 경우 대기 시간은 애플리케이션이 샘플을 엔드포인트 버퍼에 쓰는 시간부터 스피커를 통해 샘플을 들을 때까지의 최대 지연 시간입니다. 캡처 스트림의 경우 대기 시간은 소리가 마이크에 들어오는 시간부터 애플리케이션이 엔드포인트 버퍼에서 해당 소리에 대한 샘플을 읽을 수 있는 시간까지의 최대 지연 시간입니다.

전용 모드 스트림을 사용하는 애플리케이션은 오디오 엔드포인트 디바이스와 엔드포인트 버퍼에 액세스하는 애플리케이션 스레드 간의 데이터 경로에 짧은 대기 시간이 필요하기 때문에 이러한 작업을 수행하는 경우가 많습니다. 일반적으로 이러한 스레드는 상대적으로 높은 우선 순위로 실행되며 오디오 하드웨어에서 연속 처리 패스를 구분하는 주기적 간격에 가깝거나 동일한 주기적 간격으로 실행되도록 예약합니다. 각 단계에서 오디오 하드웨어는 엔드포인트 버퍼의 새 데이터를 처리합니다.

가장 작은 스트림 대기 시간을 달성하려면 애플리케이션에 특수 오디오 하드웨어와 가볍게 로드된 컴퓨터 시스템이 모두 필요할 수 있습니다. 오디오 하드웨어를 타이밍 제한을 초과하거나 우선 순위가 높은 경쟁 작업으로 시스템을 로드하면 대기 시간이 짧은 오디오 스트림에서 결함이 발생할 수 있습니다. 예를 들어 렌더링 스트림의 경우 오디오 하드웨어가 버퍼를 읽기 전에 애플리케이션이 엔드포인트 버퍼에 쓰지 못하거나 버퍼가 재생되도록 예약된 시간 전에 하드웨어가 버퍼를 읽지 못하는 경우 결함이 발생할 수 있습니다. 일반적으로 다양한 오디오 하드웨어 및 광범위한 시스템에서 실행하려는 애플리케이션은 모든 대상 환경에서 결함을 방지할 수 있을 만큼 타이밍 요구 사항을 완화해야 합니다.

Windows Vista에는 대기 시간이 짧은 오디오 스트림이 필요한 애플리케이션을 지원하는 몇 가지 기능이 있습니다. 사용자 모드 오디오 구성 요소에서 설명한 대로 시간이 중요한 작업을 수행하는 애플리케이션은 우선 순위가 낮은 애플리케이션에 대한 CPU 리소스를 거부하지 않고도 MMCSS(멀티미디어 클래스 스케줄러 서비스) 함수를 호출하여 스레드 우선 순위를 높일 수 있습니다. 또한 IAudioClient::Initialize 메서드는 애플리케이션의 버퍼 서비스 스레드가 오디오 디바이스에서 새 버퍼를 사용할 수 있게 될 때 실행되도록 예약할 수 있는 AUDCLNT_STREAMFLAGS_EVENTCALLBACK 플래그를 지원합니다. 이러한 기능을 사용하면 애플리케이션 스레드가 실행되는 시기에 대한 불확실성을 줄여 대기 시간이 짧은 오디오 스트림에서 결함의 위험을 줄일 수 있습니다.

이전 오디오 어댑터용 드라이버는 WaveCyclic 또는 WavePci DDI(디바이스 드라이버 인터페이스)를 사용할 가능성이 높으며, 최신 오디오 어댑터용 드라이버는 WaveRT DDI를 지원할 가능성이 더 높습니다. 전용 모드 애플리케이션의 경우 WaveRT 드라이버는 WaveCyclic 또는 WavePci 드라이버보다 더 나은 성능을 제공할 수 있지만 WaveRT 드라이버에는 추가 하드웨어 기능이 필요합니다. 이러한 기능에는 애플리케이션과 직접 하드웨어 버퍼를 공유하는 기능이 포함됩니다. 직접 공유를 사용하면 전용 모드 애플리케이션과 오디오 하드웨어 간에 데이터를 전송하는 데 시스템 개입이 필요하지 않습니다. 반면, WaveCyclic 및 WavePci 드라이버는 이전의 성능이 낮은 오디오 어댑터에 적합합니다. 이러한 어댑터는 시스템 소프트웨어를 사용하여 애플리케이션 버퍼와 하드웨어 버퍼 간에 데이터 블록(시스템 I/O 요청 패킷 또는 IRP에 연결됨)을 전송합니다. 또한 USB 오디오 디바이스는 시스템 소프트웨어를 사용하여 애플리케이션 버퍼와 하드웨어 버퍼 간에 데이터를 전송합니다. 데이터 전송을 위해 시스템을 사용하는 오디오 디바이스에 연결하는 전용 모드 애플리케이션의 성능을 향상시키기 위해 WASAPI는 애플리케이션과 하드웨어 간에 데이터를 전송하는 시스템 스레드의 우선 순위를 자동으로 높입니다. WASAPI는 MMCSS를 사용하여 스레드 우선 순위를 높입니다. Windows Vista에서 시스템 스레드가 PCM 형식 및 디바이스 기간이 10밀리초 미만인 전용 모드 오디오 재생 스트림에 대한 데이터 전송을 관리하는 경우 WASAPI는 MMCSS 작업 이름 "Pro Audio"를 스레드에 할당합니다. 스트림의 디바이스 기간이 10밀리초보다 크거나 같은 경우 WASAPI는 MMCSS 작업 이름 "Audio"를 스레드에 할당합니다. WaveCyclic, WavePci 및 WaveRT DDI에 대한 자세한 내용은 Windows DDK 설명서를 참조하세요. 적절한 디바이스 기간을 선택하는 방법에 대한 자세한 내용은 IAudioClient::GetDevicePeriod를 참조하세요.

세션 볼륨 제어에 설명된 대로 WASAPI는 공유 모드 오디오 스트림의 볼륨 수준을 제어하기 위한 ISimpleAudioVolume, IChannelAudioVolumeIAudioStreamVolume 인터페이스를 제공합니다. 그러나 이러한 인터페이스의 컨트롤은 배타적 모드 스트림에 영향을 주지 않습니다. 대신 전용 모드 스트림을 관리하는 애플리케이션은 일반적으로 EndpointVolume APIIAudioEndpointVolume 인터페이스를 사용하여 해당 스트림의 볼륨 수준을 제어합니다. 이 인터페이스에 대한 자세한 내용은 엔드포인트 볼륨 컨트롤을 참조하세요.

시스템의 각 재생 디바이스 및 캡처 디바이스에 대해 사용자는 전용 모드에서 디바이스를 사용할 수 있는지 여부를 제어할 수 있습니다. 사용자가 디바이스의 단독 모드 사용을 사용하지 않도록 설정하는 경우 디바이스를 사용하여 공유 모드 스트림만 재생하거나 기록할 수 있습니다.

사용자가 디바이스의 단독 모드 사용을 사용하도록 설정하는 경우 사용자는 애플리케이션에서 전용 모드로 디바이스를 사용하도록 요청하면 현재 디바이스를 통해 공유 모드 스트림을 재생하거나 기록할 수 있는 애플리케이션에서 디바이스 사용을 선점할지 여부를 제어할 수 있습니다. 선점이 활성화된 경우 디바이스가 현재 사용되지 않거나 디바이스가 공유 모드에서 사용되고 있는 경우 애플리케이션이 디바이스를 단독으로 제어하도록 요청하는 데 성공하지만 다른 애플리케이션이 이미 디바이스를 단독으로 제어할 수 있는 경우 요청이 실패합니다. 선점이 비활성화된 경우 디바이스가 현재 사용되지 않는 경우 애플리케이션에서 디바이스를 단독으로 제어하라는 요청이 성공하지만 디바이스가 공유 모드 또는 배타적 모드에서 이미 사용 중인 경우 요청이 실패합니다.

Windows Vista에서 오디오 엔드포인트 디바이스의 기본 설정은 다음과 같습니다.

  • 디바이스를 사용하여 단독 모드 스트림을 재생하거나 기록할 수 있습니다.
  • 디바이스를 사용하여 단독 모드 스트림을 재생하거나 기록하라는 요청은 현재 디바이스를 통해 재생되거나 기록되고 있는 모든 공유 모드 스트림을 선점합니다.

재생 또는 녹화 디바이스의 단독 모드 설정을 변경하려면

  1. 작업 표시줄의 오른쪽에 있는 알림 영역에서 스피커 아이콘을 마우스 오른쪽 단추로 클릭하고 장치 재생 또는 장치 녹화를 선택합니다. 또는 명령 프롬프트 창에서 Windows 멀티미디어 제어판 Mmsys.cpl 실행합니다. 자세한 내용은 DEVICE_STATE_XXX 상수의 설명을 참조하세요.)
  2. 소리 창이 나타나면 재생 또는 녹음/녹화를 선택합니다. 다음으로 디바이스 이름 목록에서 항목을 선택하고 속성을 클릭합니다.
  3. 속성 창이 나타나면 고급을 클릭합니다.
  4. 애플리케이션이 전용 모드에서 디바이스를 사용할 수 있도록 하려면 애플리케이션이 이 디바이스를 단독으로 제어할 수 있도록 허용 상자에 검사. 디바이스의 배타적 모드 사용을 사용하지 않도록 설정하려면 검사 상자를 선택 취소합니다.
  5. 디바이스의 단독 모드 사용을 사용하도록 설정한 경우 디바이스가 현재 재생 중이거나 공유 모드 스트림을 기록하는 경우 디바이스의 배타적 제어 요청이 성공할지 여부를 지정할 수 있습니다. 배타적 모드 애플리케이션에 공유 모드 애플리케이션보다 우선 순위를 부여하려면 배타적 모드 애플리케이션 우선 순위 지정 상자에 검사. 공유 모드 애플리케이션보다 배타적 모드 애플리케이션 우선 순위를 거부하려면 검사 상자를 선택 취소합니다.

다음 코드 예제에서는 전용 모드에서 사용하도록 구성된 오디오 렌더링 디바이스에서 대기 시간이 짧은 오디오 스트림을 재생하는 방법을 보여줍니다.

//-----------------------------------------------------------
// Play an exclusive-mode stream on the default audio
// rendering device. The PlayExclusiveStream function uses
// event-driven buffering and MMCSS to play the stream at
// the minimum latency supported by the device.
//-----------------------------------------------------------

// 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 PlayExclusiveStream(MyAudioSource *pMySource)
{
    HRESULT hr;
    REFERENCE_TIME hnsRequestedDuration = 0;
    IMMDeviceEnumerator *pEnumerator = NULL;
    IMMDevice *pDevice = NULL;
    IAudioClient *pAudioClient = NULL;
    IAudioRenderClient *pRenderClient = NULL;
    WAVEFORMATEX *pwfx = NULL;
    HANDLE hEvent = NULL;
    HANDLE hTask = NULL;
    UINT32 bufferFrameCount;
    BYTE *pData;
    DWORD flags = 0;
    DWORD taskIndex = 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)

    // Call a helper function to negotiate with the audio
    // device for an exclusive-mode stream format.
    hr = GetStreamFormat(pAudioClient, &pwfx);
    EXIT_ON_ERROR(hr)

    // Initialize the stream to play at the minimum latency.
    hr = pAudioClient->GetDevicePeriod(NULL, &hnsRequestedDuration);
    EXIT_ON_ERROR(hr)

    hr = pAudioClient->Initialize(
                         AUDCLNT_SHAREMODE_EXCLUSIVE,
                         AUDCLNT_STREAMFLAGS_EVENTCALLBACK,
                         hnsRequestedDuration,
                         hnsRequestedDuration,
                         pwfx,
                         NULL);
    if (hr == AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED) {
        // Align the buffer if needed, see IAudioClient::Initialize() documentation
        UINT32 nFrames = 0;
        hr = pAudioClient->GetBufferSize(&nFrames);
        EXIT_ON_ERROR(hr)
        hnsRequestedDuration = (REFERENCE_TIME)((double)REFTIMES_PER_SEC / pwfx->nSamplesPerSec * nFrames + 0.5);
        hr = pAudioClient->Initialize(
            AUDCLNT_SHAREMODE_EXCLUSIVE,
            AUDCLNT_STREAMFLAGS_EVENTCALLBACK,
            hnsRequestedDuration,
            hnsRequestedDuration,
            pwfx,
            NULL);
    }
    EXIT_ON_ERROR(hr)

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

    // Create an event handle and register it for
    // buffer-event notifications.
    hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
    if (hEvent == NULL)
    {
        hr = E_FAIL;
        goto Exit;
    }

    hr = pAudioClient->SetEventHandle(hEvent);
    EXIT_ON_ERROR(hr);

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

    hr = pAudioClient->GetService(
                         IID_IAudioRenderClient,
                         (void**)&pRenderClient);
    EXIT_ON_ERROR(hr)

    // To reduce latency, load the first buffer with data
    // from the audio source before starting the stream.
    hr = pRenderClient->GetBuffer(bufferFrameCount, &pData);
    EXIT_ON_ERROR(hr)

    hr = pMySource->LoadData(bufferFrameCount, pData, &flags);
    EXIT_ON_ERROR(hr)

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

    // Ask MMCSS to temporarily boost the thread priority
    // to reduce glitches while the low-latency stream plays.
    hTask = AvSetMmThreadCharacteristics(TEXT("Pro Audio"), &taskIndex);
    if (hTask == NULL)
    {
        hr = E_FAIL;
        EXIT_ON_ERROR(hr)
    }

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

    // Each loop fills one of the two buffers.
    while (flags != AUDCLNT_BUFFERFLAGS_SILENT)
    {
        // Wait for next buffer event to be signaled.
        DWORD retval = WaitForSingleObject(hEvent, 2000);
        if (retval != WAIT_OBJECT_0)
        {
            // Event handle timed out after a 2-second wait.
            pAudioClient->Stop();
            hr = ERROR_TIMEOUT;
            goto Exit;
        }

        // Grab the next empty buffer from the audio device.
        hr = pRenderClient->GetBuffer(bufferFrameCount, &pData);
        EXIT_ON_ERROR(hr)

        // Load the buffer with data from the audio source.
        hr = pMySource->LoadData(bufferFrameCount, pData, &flags);
        EXIT_ON_ERROR(hr)

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

    // Wait for the last buffer to play before stopping.
    Sleep((DWORD)(hnsRequestedDuration/REFTIMES_PER_MILLISEC));

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

Exit:
    if (hEvent != NULL)
    {
        CloseHandle(hEvent);
    }
    if (hTask != NULL)
    {
        AvRevertMmThreadCharacteristics(hTask);
    }
    CoTaskMemFree(pwfx);
    SAFE_RELEASE(pEnumerator)
    SAFE_RELEASE(pDevice)
    SAFE_RELEASE(pAudioClient)
    SAFE_RELEASE(pRenderClient)

    return hr;
}

앞의 코드 예제에서 PlayExclusiveStream 함수는 렌더링 스트림이 재생되는 동안 엔드포인트 버퍼를 서비스하는 애플리케이션 스레드에서 실행됩니다. 함수는 클라이언트 정의 클래스 MyAudioSource에 속하는 개체에 대한 포인터인 단일 매개 변수 pMySource를 사용합니다. 이 클래스에는 코드 예제에서 호출되는 LoadData 및 SetFormat이라는 두 개의 멤버 함수가 있습니다. MyAudioSource는 스트림 렌더링에 설명되어 있습니다.

PlayExclusiveStream 함수는 기본 렌더링 디바이스와 협상하는 도우미 함수 GetStreamFormat을 호출하여 디바이스가 애플리케이션에서 사용하기에 적합한 독점 모드 스트림 형식을 지원하는지 여부를 결정합니다. GetStreamFormat 함수의 코드는 코드 예제에 표시되지 않습니다. 구현의 세부 정보는 애플리케이션의 요구 사항에 전적으로 의존하기 때문입니다. 그러나 GetStreamFormat 함수의 작업은 간단히 설명할 수 있습니다. IAudioClient::IsFormatSupported 메서드를 한 번 이상 호출하여 디바이스가 적절한 형식을 지원하는지 여부를 확인합니다. 애플리케이션의 요구 사항에 따라 GetStreamFormat이 IsFormatSupported 메서드에 제공하는 형식과 이를 표시하는 순서가 지정됩니다. IsFormatSupported에 대한 자세한 내용은 디바이스 형식을 참조하세요.

GetStreamFormat 호출 후 PlayExclusiveStream 함수는 IAudioClient::GetDevicePeriod 메서드를 호출하여 오디오 하드웨어에서 지원하는 최소 디바이스 기간을 가져옵니다. 다음으로, 함수는 IAudioClient::Initialize 메서드를 호출하여 최소 기간과 동일한 버퍼 기간을 요청합니다. 호출이 성공하면 Initialize 메서드는 두 개의 엔드포인트 버퍼를 할당하며, 각 엔드포인트 버퍼는 최소 기간의 기간과 같습니다. 나중에 오디오 스트림이 실행되기 시작하면 애플리케이션 및 오디오 하드웨어는 두 버퍼를 "ping-pong" 방식으로 공유합니다. 즉, 애플리케이션이 하나의 버퍼에 쓰는 동안 하드웨어는 다른 버퍼에서 읽습니다.

스트림을 시작하기 전에 PlayExclusiveStream 함수는 다음을 수행합니다.

  • 버퍼를 채울 준비가 되면 알림을 받을 이벤트 핸들을 만들고 등록합니다.
  • 첫 번째 버퍼를 오디오 원본의 데이터로 채우면 스트림이 실행하기 시작하는 시점부터 초기 소리가 들릴 때까지의 지연을 줄입니다.
  • AvSetMmThreadCharacteristics 함수를 호출하여 MMCSS가 PlayExclusiveStream이 실행되는 스레드의 우선 순위를 높이도록 요청합니다. 스트림 실행이 중지되면 AvRevertMmThreadCharacteristics 함수 호출은 원래 스레드 우선 순위를 복원합니다.

AvSetMmThreadCharacteristicsAvRevertMmThreadCharacteristics에 대한 자세한 내용은 Windows SDK 설명서를 참조하세요.

스트림이 실행되는 동안 앞의 코드 예제에서 while 루프의 각 반복은 하나의 엔드포인트 버퍼를 채웁니다. 반복 사이에 WaitForSingleObject 함수 호출은 이벤트 핸들이 신호를 받을 때까지 기다립니다. 핸들이 신호를 받으면 루프 본문은 다음을 수행합니다.

  1. IAudioRenderClient::GetBuffer 메서드를 호출하여 다음 버퍼를 가져옵니다.
  2. 버퍼를 채웁니다.
  3. IAudioRenderClient::ReleaseBuffer 메서드를 호출하여 버퍼를 해제합니다.

WaitForSingleObject에 대한 자세한 내용은 Windows SDK 설명서를 참조하세요.

오디오 어댑터가 WaveRT 드라이버에 의해 제어되는 경우 이벤트 핸들의 신호는 오디오 하드웨어의 DMA 전송 알림에 연결됩니다. USB 오디오 디바이스 또는 WaveCyclic 또는 WavePci 드라이버로 제어되는 오디오 디바이스의 경우 이벤트 핸들의 신호는 애플리케이션 버퍼에서 하드웨어 버퍼로 데이터를 전송하는 IRP의 완료와 연결됩니다.

앞의 코드 예제에서는 오디오 하드웨어와 컴퓨터 시스템을 성능 제한으로 푸시합니다. 먼저 스트림 대기 시간을 줄이기 위해 애플리케이션은 오디오 하드웨어에서 지원하는 최소 디바이스 기간을 사용하도록 버퍼 서비스 스레드를 예약합니다. 둘째, 스레드가 각 디바이스 기간 내에 안정적으로 실행되도록 하기 위해 AvSetMmThreadCharacteristics 함수 호출은 TaskName 매개 변수를 "Pro Audio"로 설정합니다. 즉, Windows Vista에서 우선 순위가 가장 높은 기본 작업 이름입니다. 애플리케이션의 타이밍 요구 사항이 유용성을 손상시키지 않고 완화될 수 있는지 여부를 고려합니다. 예를 들어 애플리케이션은 최소보다 긴 기간을 사용하도록 버퍼 서비스 스레드를 예약할 수 있습니다. 더 긴 기간은 스레드 우선 순위가 낮을 때 안전하게 사용할 수 있습니다.

스트림 관리