다음을 통해 공유


공간 오디오 개체를 사용하여 공간 소리 렌더링

이 문서에서는 정적 공간 오디오 개체, 동적 공간 오디오 개체 및 Microsoft의 HRTF(Head Relative Transfer Function)를 사용하는 공간 오디오 개체를 사용하여 공간 사운드를 구현하는 방법을 보여 주는 몇 가지 간단한 예제를 제공합니다. 이러한 세 가지 기술에 대한 구현 단계는 매우 유사하며 이 문서에서는 각 기술에 대해 비슷하게 구조화된 코드 예제를 제공합니다. 실제 공간 오디오 구현의 전체 엔드투엔드 예제는 Microsoft Spatial Sound 샘플 github 리포지토리참조하세요. Xbox 및 Windows에서 공간 사운드 지원을 위한 Microsoft의 플랫폼 수준 솔루션인 Windows Sonic에 대한 개요는 Spatial Sound참조하세요.

정적 공간 오디오 개체를 사용하여 오디오 렌더링

정적 오디오 개체는 AudioObjectType 열거형에 정의된 18개의 정적 오디오 채널 중 하나에 소리를 렌더링하는 데 사용됩니다. 이러한 각 채널은 시간이 지남에 따라 이동하지 않는 고정된 공간의 실제 또는 가상화된 스피커를 나타냅니다. 특정 디바이스에서 사용할 수 있는 정적 채널은 사용 중인 공간 사운드 형식에 따라 달라집니다. 지원되는 형식 및 해당 채널 수 목록은 Spatial Sound참조하세요.

공간 오디오 스트림을 초기화할 때 스트림에서 사용할 사용 가능한 정적 채널을 지정해야 합니다. 다음 예제 상수 정의를 사용하여 공통 스피커 구성을 지정하고 각 구성에 사용할 수 있는 채널 수를 가져올 수 있습니다.

const AudioObjectType ChannelMask_Mono = AudioObjectType_FrontCenter;
const AudioObjectType ChannelMask_Stereo = (AudioObjectType)(AudioObjectType_FrontLeft | AudioObjectType_FrontRight);
const AudioObjectType ChannelMask_2_1 = (AudioObjectType)(ChannelMask_Stereo | AudioObjectType_LowFrequency);
const AudioObjectType ChannelMask_Quad = (AudioObjectType)(AudioObjectType_FrontLeft | AudioObjectType_FrontRight | AudioObjectType_BackLeft | AudioObjectType_BackRight);
const AudioObjectType ChannelMask_4_1 = (AudioObjectType)(ChannelMask_Quad | AudioObjectType_LowFrequency);
const AudioObjectType ChannelMask_5_1 = (AudioObjectType)(AudioObjectType_FrontLeft | AudioObjectType_FrontRight | AudioObjectType_FrontCenter | AudioObjectType_LowFrequency | AudioObjectType_SideLeft | AudioObjectType_SideRight);
const AudioObjectType ChannelMask_7_1 = (AudioObjectType)(ChannelMask_5_1 | AudioObjectType_BackLeft | AudioObjectType_BackRight);

const UINT32 MaxStaticObjectCount_7_1_4 = 12;
const AudioObjectType ChannelMask_7_1_4 = (AudioObjectType)(ChannelMask_7_1 | AudioObjectType_TopFrontLeft | AudioObjectType_TopFrontRight | AudioObjectType_TopBackLeft | AudioObjectType_TopBackRight);

const UINT32 MaxStaticObjectCount_7_1_4_4 = 16;
const AudioObjectType ChannelMask_7_1_4_4 = (AudioObjectType)(ChannelMask_7_1_4 | AudioObjectType_BottomFrontLeft | AudioObjectType_BottomFrontRight |AudioObjectType_BottomBackLeft | AudioObjectType_BottomBackRight);

const UINT32 MaxStaticObjectCount_8_1_4_4 = 17;
const AudioObjectType ChannelMask_8_1_4_4 = (AudioObjectType)(ChannelMask_7_1_4_4 | AudioObjectType_BackCenter);

공간 오디오를 렌더링하는 첫 번째 단계는 오디오 데이터를 보낼 오디오 엔드포인트를 가져오는 것입니다. MMDeviceEnumerator 인스턴스를 만들고 GetDefaultAudioEndpoint 호출하여 기본 오디오 렌더링 디바이스를 가져옵니다.

HRESULT hr;
Microsoft::WRL::ComPtr<IMMDeviceEnumerator> deviceEnum;
Microsoft::WRL::ComPtr<IMMDevice> defaultDevice;

hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, __uuidof(IMMDeviceEnumerator), (void**)&deviceEnum);
hr = deviceEnum->GetDefaultAudioEndpoint(EDataFlow::eRender, eMultimedia, &defaultDevice);

공간 오디오 스트림을 만들 때 WAVEFORMATEX 구조를 제공하여 스트림에서 사용할 오디오 형식을 지정해야 합니다. 파일에서 오디오를 재생하는 경우 형식은 일반적으로 오디오 파일 형식에 따라 결정됩니다. 이 예제에서는 모노, 32비트, 48Hz 형식을 사용합니다.

WAVEFORMATEX format;
format.wFormatTag = WAVE_FORMAT_IEEE_FLOAT;
format.wBitsPerSample = 32;
format.nChannels = 1;
format.nSamplesPerSec = 48000;
format.nBlockAlign = (format.wBitsPerSample >> 3) * format.nChannels;
format.nAvgBytesPerSec = format.nBlockAlign * format.nSamplesPerSec;
format.cbSize = 0;

공간 오디오를 렌더링하는 다음 단계는 공간 오디오 스트림을 초기화하는 것입니다. 먼저 IMMDevice::Activate호출하여 ISpatialAudioClient 인스턴스를 가져옵니다. ISpatialAudioClient::IsAudioObjectFormatSupported 호출하여 사용 중인 오디오 형식이 지원되는지 확인합니다. 오디오 파이프라인이 더 많은 오디오 데이터에 대한 준비가 되었음을 앱에 알리는 데 사용할 이벤트를 만듭니다.

공간 오디오 스트림을 초기화하기 위해 사용할 SpatialAudioObjectRenderStreamActivationParams 구조체를 채우십시오. 이 예제에서 StaticObjectTypeMask 필드는 이 문서의 앞부분에서 정의된 ChannelMask_Stereo 상수로 설정됩니다. 즉, 오디오 스트림에서 오른쪽 및 왼쪽 앞 채널만 사용할 수 있습니다. 이 예제에서는 정적 오디오 개체만 사용하고 동적 개체는 사용하지 않으므로 MaxDynamicObjectCount 필드는 0으로 설정됩니다. 범주 필드는 시스템이 이 스트림의 소리를 다른 오디오 소스와 혼합하는 방법을 정의하는 AUDIO_STREAM_CATEGORY 열거형의 멤버로 설정됩니다.

ISpatialAudioClient::ActivateSpatialAudioStream 호출하여 스트림을 활성화합니다.

Microsoft::WRL::ComPtr<ISpatialAudioClient> spatialAudioClient;

// Activate ISpatialAudioClient on the desired audio-device 
hr = defaultDevice->Activate(__uuidof(ISpatialAudioClient), CLSCTX_INPROC_SERVER, nullptr, (void**)&spatialAudioClient);

hr = spatialAudioClient->IsAudioObjectFormatSupported(&format);

// Create the event that will be used to signal the client for more data
HANDLE bufferCompletionEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);

SpatialAudioObjectRenderStreamActivationParams streamParams;
streamParams.ObjectFormat = &format;
streamParams.StaticObjectTypeMask = ChannelMask_Stereo;
streamParams.MinDynamicObjectCount = 0;
streamParams.MaxDynamicObjectCount = 0;
streamParams.Category = AudioCategory_SoundEffects;
streamParams.EventHandle = bufferCompletionEvent;
streamParams.NotifyObject = nullptr;

PROPVARIANT activationParams;
PropVariantInit(&activationParams);
activationParams.vt = VT_BLOB;
activationParams.blob.cbSize = sizeof(streamParams);
activationParams.blob.pBlobData = reinterpret_cast<BYTE *>(&streamParams);

Microsoft::WRL::ComPtr<ISpatialAudioObjectRenderStream> spatialAudioStream;
hr = spatialAudioClient->ActivateSpatialAudioStream(&activationParams, __uuidof(spatialAudioStream), (void**)&spatialAudioStream);

메모

Xbox One 개발 키트(XDK) 타이틀에서 ISpatialAudioClient 인터페이스를 사용하는 경우 먼저 IMMDeviceEnumerator::EnumAudioEndpoints 또는 IMMDeviceEnumerator::GetDefaultAudioEndpoint호출하기 전에 EnableSpatialAudio 호출해야 합니다. 이렇게 하지 않으면 활성화 호출에서 E_NOINTERFACE 오류가 반환됩니다. EnableSpatialAudio XDK 타이틀에만 사용할 수 있으며 Xbox One에서 실행되는 유니버설 Windows 플랫폼 앱이나 Xbox One 이외의 장치에 대해서는 호출할 필요가 없습니다.

 

정적 채널에 오디오 데이터를 쓰는 데 사용할 ISpatialAudioObject 대한 포인터를 선언합니다. 일반적인 앱은 StaticObjectTypeMask 필드에 지정된 각 채널에 대해 개체를 사용합니다. 간단히 하기 위해 이 예제에서는 단일 정적 오디오 개체만 사용합니다.

// In this simple example, one object will be rendered
Microsoft::WRL::ComPtr<ISpatialAudioObject> audioObjectFrontLeft;

오디오 렌더링 루프를 입력하기 전에 ISpatialAudioObjectRenderStream::Start 호출하여 미디어 파이프라인에 오디오 데이터 요청을 시작하도록 지시합니다. 이 예제에서는 카운터를 사용하여 5초 후에 오디오 렌더링을 중지합니다.

렌더링 루프 내에서 공간 오디오 스트림이 초기화될 때 제공된 버퍼 완성 이벤트가 신호를 받을 때까지 기다립니다. 렌더링 유형 또는 엔드포인트가 변경되면 해당 이벤트가 신호를 받지 않으므로 이벤트를 대기할 때 100ms와 같은 적절한 시간 제한(예: 100ms)을 설정해야 합니다. 이 경우 ISpatialAudioObjectRenderStream::Reset 호출하여 공간 오디오 스트림을 다시 설정할 수 있습니다.

다음으로, ISpatialAudioObjectRenderStream::BeginUpdatingAudioObjects 호출하여 오디오 개체의 버퍼를 데이터로 채우려고 한다는 것을 시스템에 알릴 수 있습니다. 이 메서드는 이 예제에서 사용되지 않는 사용 가능한 동적 오디오 개체의 수와 이 스트림에서 렌더링된 오디오 개체에 대한 버퍼의 프레임 수를 반환합니다.

정적 공간 오디오 개체가 아직 만들어지지 않은 경우 ISpatialAudioObjectRenderStream::ActivateSpatialAudioObject호출하여 개체의 오디오가 렌더링되는 정적 채널을 나타내는 AudioObjectType 열거형의 값을 전달하여 하나 이상을 만듭니다.

다음으로, ISpatialAudioObject::GetBuffer 호출하여 공간 오디오 개체의 오디오 버퍼에 대한 포인터를 가져옵니다. 이 메서드는 버퍼의 크기(바이트)도 반환합니다. 이 예제에서는 WriteToAudioObjectBuffer 도우미 메서드를 사용하여 버퍼를 오디오 데이터로 채웁니다. 이 메서드는 이 문서의 뒷부분에 나와 있습니다. 버퍼에 데이터를 쓴 후, 이 예제는 개체의 5초 수명에 도달했는지를 확인하고, 그렇다면 ISpatialAudioObject::SetEndOfStream를 호출하여 오디오 파이프라인에 이 개체를 사용하여 더 이상의 오디오가 기록되지 않을 것임을 알리고, 개체를 nullptr로 설정하여 리소스를 해제합니다.

모든 오디오 개체에 데이터를 쓴 후 ISpatialAudioObjectRenderStream::EndUpdatingAudioObjects 호출하여 데이터가 렌더링할 준비가 되었는지 시스템에 알릴 수 있습니다. 오직 BeginUpdatingAudioObjects 호출과 EndUpdatingAudioObjects호출 사이에서만 GetBuffer를 호출할 수 있습니다.

// Start streaming / rendering 
hr = spatialAudioStream->Start();

// This example will render 5 seconds of audio samples
UINT totalFrameCount = format.nSamplesPerSec * 5;

bool isRendering = true;
while (isRendering)
{
    // Wait for a signal from the audio-engine to start the next processing pass
    if (WaitForSingleObject(bufferCompletionEvent, 100) != WAIT_OBJECT_0)
    {
        hr = spatialAudioStream->Reset();

        if (hr != S_OK)
        {
            // handle the error
            break;
        }
    }

    UINT32 availableDynamicObjectCount;
    UINT32 frameCount;

    // Begin the process of sending object data and metadata
    // Get the number of dynamic objects that can be used to send object-data
    // Get the frame count that each buffer will be filled with 
    hr = spatialAudioStream->BeginUpdatingAudioObjects(&availableDynamicObjectCount, &frameCount);

    BYTE* buffer;
    UINT32 bufferLength;

    if (audioObjectFrontLeft == nullptr)
    {
        hr = spatialAudioStream->ActivateSpatialAudioObject(AudioObjectType::AudioObjectType_FrontLeft, &audioObjectFrontLeft);
        if (hr != S_OK) break;
    }

    // Get the buffer to write audio data
    hr = audioObjectFrontLeft->GetBuffer(&buffer, &bufferLength);

    if (totalFrameCount >= frameCount)
    {
        // Write audio data to the buffer
        WriteToAudioObjectBuffer(reinterpret_cast<float*>(buffer), frameCount, 200.0f, format.nSamplesPerSec);

        totalFrameCount -= frameCount;
    }
    else
    {
        // Write audio data to the buffer
        WriteToAudioObjectBuffer(reinterpret_cast<float*>(buffer), totalFrameCount, 750.0f, format.nSamplesPerSec);

        // Set end of stream for the last buffer 
        hr = audioObjectFrontLeft->SetEndOfStream(totalFrameCount);

        audioObjectFrontLeft = nullptr; // Release the object

        isRendering = false;
    }

    // Let the audio engine know that the object data are available for processing now
    hr = spatialAudioStream->EndUpdatingAudioObjects();
};

공간 오디오 렌더링이 완료되면 ISpatialAudioObjectRenderStream::stop호출하여 공간 오디오 스트림을 중지합니다. 스트림을 다시 사용하지 않을 경우 ISpatialAudioObjectRenderStream::Reset호출하여 리소스를 해제합니다.

// Stop the stream
hr = spatialAudioStream->Stop();

// Don't want to start again, so reset the stream to free its resources
hr = spatialAudioStream->Reset();

CloseHandle(bufferCompletionEvent);

WriteToAudioObjectBuffer 도우미 메서드는 전체 샘플 버퍼 또는 앱 정의 시간 제한에 지정된 나머지 샘플 수를 기록합니다. 예를 들어 원본 오디오 파일에 남아 있는 샘플 수에 따라 확인할 수도 있습니다. 빈도 입력 매개 변수로 크기가 조정되는 단순 sin wave가 생성되어 버퍼에 기록됩니다.

void WriteToAudioObjectBuffer(FLOAT* buffer, UINT frameCount, FLOAT frequency, UINT samplingRate)
{
    const double PI = 4 * atan2(1.0, 1.0);
    static double _radPhase = 0.0;

    double step = 2 * PI * frequency / samplingRate;

    for (UINT i = 0; i < frameCount; i++)
    {
        double sample = sin(_radPhase);

        buffer[i] = FLOAT(sample);

        _radPhase += step; // next frame phase

        if (_radPhase >= 2 * PI)
        {
            _radPhase -= 2 * PI;
        }
    }
}

동적 공간 오디오 개체를 사용하여 오디오 렌더링

동적 개체를 사용하면 사용자를 기준으로 공간의 임의의 위치에서 오디오를 렌더링할 수 있습니다. 동적 오디오 개체의 위치와 볼륨은 시간이 지남에 따라 변경할 수 있습니다. 게임은 일반적으로 게임 세계에서 3D 개체의 위치를 사용하여 연결된 동적 오디오 개체의 위치를 지정합니다. 다음 예제에서는 개체를 나타내는 데 필요한 최소 데이터 집합을 저장하기 위해 My3dObject 간단한 구조를 사용합니다. 이 데이터에는 ISpatialAudioObject대한 포인터, 개체의 위치, 속도, 볼륨 및 톤 빈도 및 개체가 소리를 렌더링해야 하는 총 프레임 수를 저장하는 값이 포함됩니다.

struct My3dObject
{
    Microsoft::WRL::ComPtr<ISpatialAudioObject> audioObject;
    Windows::Foundation::Numerics::float3 position;
    Windows::Foundation::Numerics::float3 velocity;
    float volume;
    float frequency; // in Hz
    UINT totalFrameCount;
};

동적 오디오 개체에 대한 구현 단계는 위에서 설명한 정적 오디오 개체의 단계와 거의 동일합니다. 먼저 오디오 엔드포인트를 가져옵니다.

HRESULT hr;
Microsoft::WRL::ComPtr<IMMDeviceEnumerator> deviceEnum;
Microsoft::WRL::ComPtr<IMMDevice> defaultDevice;

hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, __uuidof(IMMDeviceEnumerator), (void**)&deviceEnum);
hr = deviceEnum->GetDefaultAudioEndpoint(EDataFlow::eRender, eMultimedia, &defaultDevice);

다음으로, 공간 오디오 스트림을 초기화합니다. IMMDevice::Activate호출하여 ISpatialAudioClient 인스턴스를 가져옵니다. ISpatialAudioClient::IsAudioObjectFormatSupported 호출하여 사용 중인 오디오 형식이 지원되는지 확인합니다. 오디오 파이프라인이 더 많은 오디오 데이터에 대한 준비가 되었음을 앱에 알리는 데 사용할 이벤트를 만듭니다.

ISpatialAudioClient::GetMaxDynamicObjectCount 호출하여 시스템에서 지원하는 동적 개체의 수를 검색합니다. 이 호출이 0을 반환하면 동적 공간 오디오 개체가 현재 디바이스에서 지원되거나 활성화되지 않습니다. 공간 오디오를 사용하도록 설정하는 방법과 다양한 공간 오디오 형식에 사용할 수 있는 동적 오디오 개체의 수에 대한 자세한 내용은 공간 사운드 참조하세요.

SpatialAudioObjectRenderStreamActivationParams 구조를 채울 때 MaxDynamicObjectCount 필드를 앱에서 사용할 최대 동적 개체 수로 설정합니다.

ISpatialAudioClient::ActivateSpatialAudioStream 호출하여 스트림을 활성화합니다.

// Activate ISpatialAudioClient on the desired audio-device 
Microsoft::WRL::ComPtr<ISpatialAudioClient> spatialAudioClient;
hr = defaultDevice->Activate(__uuidof(ISpatialAudioClient), CLSCTX_INPROC_SERVER, nullptr, (void**)&spatialAudioClient);

hr = spatialAudioClient->IsAudioObjectFormatSupported(&format);

// Create the event that will be used to signal the client for more data
HANDLE bufferCompletionEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);

UINT32 maxDynamicObjectCount;
hr = spatialAudioClient->GetMaxDynamicObjectCount(&maxDynamicObjectCount);

if (maxDynamicObjectCount == 0)
{
    // Dynamic objects are unsupported
    return;
}

// Set the maximum number of dynamic audio objects that will be used
SpatialAudioObjectRenderStreamActivationParams streamParams;
streamParams.ObjectFormat = &format;
streamParams.StaticObjectTypeMask = AudioObjectType_None;
streamParams.MinDynamicObjectCount = 0;
streamParams.MaxDynamicObjectCount = min(maxDynamicObjectCount, 4);
streamParams.Category = AudioCategory_GameEffects;
streamParams.EventHandle = bufferCompletionEvent;
streamParams.NotifyObject = nullptr;

PROPVARIANT pv;
PropVariantInit(&pv);
pv.vt = VT_BLOB;
pv.blob.cbSize = sizeof(streamParams);
pv.blob.pBlobData = (BYTE *)&streamParams;

Microsoft::WRL::ComPtr<ISpatialAudioObjectRenderStream> spatialAudioStream;
hr = spatialAudioClient->ActivateSpatialAudioStream(&pv, __uuidof(spatialAudioStream), (void**)&spatialAudioStream);

다음은 임의로 배치된 오디오 개체를 동적으로 생성하고 벡터에 저장하는 이 예제를 지원하는 데 필요한 일부 앱별 코드입니다.

// Used for generating a vector of randomized My3DObject structs
std::vector<My3dObject> objectVector;
std::default_random_engine gen;
std::uniform_real_distribution<> pos_dist(-25, 25); // uniform distribution for random position
std::uniform_real_distribution<> vel_dist(-1, 1); // uniform distribution for random velocity
std::uniform_real_distribution<> vol_dist(0.5, 1.0); // uniform distribution for random volume
std::uniform_real_distribution<> pitch_dist(40, 400); // uniform distribution for random pitch
int spawnCounter = 0;

오디오 렌더링 루프를 입력하기 전에 ISpatialAudioObjectRenderStream::Start 호출하여 미디어 파이프라인에 오디오 데이터 요청을 시작하도록 지시합니다.

렌더링 루프 내에서 공간 오디오 스트림이 신호를 받을 수 있도록 초기화되었을 때 제공한 버퍼 완성 이벤트를 기다립니다. 렌더링 유형 또는 엔드포인트가 변경되면 해당 이벤트가 신호를 받지 않으므로 이벤트를 대기할 때 100ms와 같은 적절한 시간 제한(예: 100ms)을 설정해야 합니다. 이 경우 ISpatialAudioObjectRenderStream::Reset 호출하여 공간 오디오 스트림을 다시 설정할 수 있습니다.

다음으로, ISpatialAudioObjectRenderStream::BeginUpdatingAudioObjects 호출하여 오디오 개체의 버퍼를 데이터로 채우려고 한다는 것을 시스템에 알릴 수 있습니다. 이 메서드는 사용 가능한 동적 오디오 개체의 수와 이 스트림에서 렌더링된 오디오 개체에 대한 버퍼의 프레임 수를 반환합니다.

생성 카운터가 지정된 값에 도달할 때마다 AudioObjectType_Dynamic지정하는ISpatialAudioObjectRenderStream::ActivateSpatialAudioObject를 호출하여 새 동적 오디오 개체를 활성화합니다. 사용 가능한 모든 동적 오디오 개체가 이미 할당된 경우 이 메서드는 SPLAUDCLNT_E_NO_MORE_OBJECTS반환합니다. 이 경우 앱별 우선 순위에 따라 이전에 활성화된 오디오 개체를 하나 이상 해제하도록 선택할 수 있습니다. 동적 오디오 개체를 만든 후 임의 위치, 속도, 볼륨 및 빈도 값을 사용하여 새 My3dObject 구조에 추가된 다음 활성 개체 목록에 추가됩니다.

다음으로, 앱 정의 My3dObject 구조로 이 예제에 표시된 모든 활성 개체를 반복합니다. 각 오디오 개체에 대해 ISpatialAudioObject::GetBuffer 호출하여 공간 오디오 개체의 오디오 버퍼에 대한 포인터를 가져옵니다. 이 메서드는 버퍼의 크기(바이트)도 반환합니다. writeToAudioObjectBuffer 도우미 메서드는 버퍼를 오디오 데이터로 채웁니다. 버퍼에 쓴 후 예제에서는 ISpatialAudioObject::SetPosition호출하여 동적 오디오 개체의 위치를 업데이트합니다. SetVolume호출하여 오디오 개체의 볼륨을 수정할 수도 있습니다. 개체의 위치 또는 볼륨을 업데이트하지 않으면 개체가 마지막으로 설정된 시점의 위치와 볼륨이 유지됩니다. 개체의 앱 정의 수명에 도달하면, 오디오 파이프라인에 더 이상 이 개체를 사용하여 오디오가 기록되지 않음을 알리기 위해 iSpatialAudioObject::SetEndOfStream이 호출되고, 개체가 nullptr 로 설정되어 리소스를 해제합니다.

모든 오디오 개체에 데이터를 쓴 후 ISpatialAudioObjectRenderStream::EndUpdatingAudioObjects 호출하여 데이터가 렌더링할 준비가 되었는지 시스템에 알릴 수 있습니다. BeginUpdatingAudioObjectsEndUpdatingAudioObjects사이에서만 GetBuffer을 호출할 수 있습니다.

// Start streaming / rendering 
hr = spatialAudioStream->Start();

do
{
    // Wait for a signal from the audio-engine to start the next processing pass
    if (WaitForSingleObject(bufferCompletionEvent, 100) != WAIT_OBJECT_0)
    {
        break;
    }

    UINT32 availableDynamicObjectCount;
    UINT32 frameCount;

    // Begin the process of sending object data and metadata
    // Get the number of active objects that can be used to send object-data
    // Get the frame count that each buffer will be filled with 
    hr = spatialAudioStream->BeginUpdatingAudioObjects(&availableDynamicObjectCount, &frameCount);

    BYTE* buffer;
    UINT32 bufferLength;

    // Spawn a new dynamic audio object every 200 iterations
    if (spawnCounter % 200 == 0 && spawnCounter < 1000)
    {
        // Activate a new dynamic audio object
        Microsoft::WRL::ComPtr<ISpatialAudioObject> audioObject;
        hr = spatialAudioStream->ActivateSpatialAudioObject(AudioObjectType::AudioObjectType_Dynamic, &audioObject);

        // If SPTLAUDCLNT_E_NO_MORE_OBJECTS is returned, there are no more available objects
        if (SUCCEEDED(hr))
        {
            // Init new struct with the new audio object.
            My3dObject obj = {
                audioObject,
                Windows::Foundation::Numerics::float3(static_cast<float>(pos_dist(gen)), static_cast<float>(pos_dist(gen)), static_cast<float>(pos_dist(gen))),
                Windows::Foundation::Numerics::float3(static_cast<float>(vel_dist(gen)), static_cast<float>(vel_dist(gen)), static_cast<float>(vel_dist(gen))),
                static_cast<float>(static_cast<float>(vol_dist(gen))),
                static_cast<float>(static_cast<float>(pitch_dist(gen))),
                format.nSamplesPerSec * 5 // 5 seconds of audio samples
            };

            objectVector.insert(objectVector.begin(), obj);
        }
    }
    spawnCounter++;

    // Loop through all dynamic audio objects
    std::vector<My3dObject>::iterator it = objectVector.begin();
    while (it != objectVector.end())
    {
        it->audioObject->GetBuffer(&buffer, &bufferLength);

        if (it->totalFrameCount >= frameCount)
        {
            // Write audio data to the buffer
            WriteToAudioObjectBuffer(reinterpret_cast<float*>(buffer), frameCount, it->frequency, format.nSamplesPerSec);

            // Update the position and volume of the audio object
            it->audioObject->SetPosition(it->position.x, it->position.y, it->position.z);
            it->position += it->velocity;
            it->audioObject->SetVolume(it->volume);

            it->totalFrameCount -= frameCount;

            ++it;
        }
        else
        {
            // If the audio object reaches its lifetime, set EndOfStream and release the object

            // Write audio data to the buffer
            WriteToAudioObjectBuffer(reinterpret_cast<float*>(buffer), it->totalFrameCount, it->frequency, format.nSamplesPerSec);

            // Set end of stream for the last buffer 
            hr = it->audioObject->SetEndOfStream(it->totalFrameCount);

            it->audioObject = nullptr; // Release the object

            it->totalFrameCount = 0;

            it = objectVector.erase(it);
        }
    }

    // Let the audio-engine know that the object data are available for processing now
    hr = spatialAudioStream->EndUpdatingAudioObjects();
} while (objectVector.size() > 0);

공간 오디오 렌더링이 완료되면 ISpatialAudioObjectRenderStream::stop호출하여 공간 오디오 스트림을 중지합니다. 스트림을 다시 사용하지 않을 경우 ISpatialAudioObjectRenderStream::Reset호출하여 리소스를 해제합니다.

// Stop the stream 
hr = spatialAudioStream->Stop();

// We don't want to start again, so reset the stream to free it's resources.
hr = spatialAudioStream->Reset();

CloseHandle(bufferCompletionEvent);

HRTF용 동적 공간 오디오 개체를 사용하여 오디오 렌더링

또 다른 API 집합인 ISpatialAudioRenderStreamForHrtfISpatialAudioObjectForHrtf은 Microsoft의 HRTF(Head-Relative Transfer Function)를 활용하여, 사용자와의 상대적인 위치에 따라 송신기의 위치를 시뮬레이션하기 위해 소리를 감쇠하는 공간 오디오를 가능하게 합니다. 이 위치는 시간에 따라 변경될 수 있습니다. 위치 외에도 HRTF 오디오 개체를 사용하면 공간 방향, 원뿔 또는 카디오이드 모양과 같은 소리를 내보낸 직접성 및 개체가 가상 수신기에서 더 가까이 이동하고 더 멀리 이동할 때 감쇠 모델을 지정할 수 있습니다. 이러한 HRTF 인터페이스는 사용자가 디바이스의 공간 오디오 엔진으로 헤드폰용 Windows Sonic을 선택한 경우에만 사용할 수 있습니다. 헤드폰용 Windows Sonic을 사용하도록 디바이스를 구성하는 방법에 대한 자세한 내용은 Spatial Sound참조하세요.

ISpatialAudioRenderStreamForHrtfISpatialAudioObjectForHrtf API를 사용하면 애플리케이션이 헤드폰용 Windows Sonic 렌더링 경로를 직접 명시적으로 사용할 수 있습니다. 이러한 API는 홈 시어터용 Dolby Atmos 또는 헤드폰용 Dolby Atmos와 같은 공간 사운드 형식이나 사운드 제어판을 통한 소비자 제어 출력 형식 전환 또는 스피커 재생을 지원하지 않습니다. 이러한 인터페이스는 헤드폰 관련 기능에 Windows Sonic을 사용하려는 Windows Mixed Reality 애플리케이션에서 사용하기 위한 것입니다(예: 일반적인 콘텐츠 제작 파이프라인 외부에서 프로그래밍 방식으로 지정된 환경 사전 설정 및 거리 기반 롤오프). 대부분의 게임 및 가상 현실 시나리오는 ISpatialAudioClient 대신 사용하는 것을 선호합니다. 두 API 집합의 구현 단계는 거의 동일하므로 현재 디바이스에서 사용할 수 있는 기능에 따라 런타임에 두 기술을 모두 구현하고 전환할 수 있습니다.

혼합 현실 앱은 일반적으로 가상 세계에서 3D 개체의 위치를 사용하여 연결된 동적 오디오 개체의 위치를 지정합니다. 다음 예제에서는 개체를 나타내는 데 필요한 최소 데이터 집합을 저장하기 위해 My3dObjectForHrtf 간단한 구조를 사용합니다. 이 데이터에는 ISpatialAudioObjectForHrtf대한 포인터, 개체의 위치, 방향, 속도 및 톤 빈도 및 개체가 소리를 렌더링해야 하는 총 프레임 수를 저장하는 값이 포함됩니다.

struct My3dObjectForHrtf
{
    Microsoft::WRL::ComPtr<ISpatialAudioObjectForHrtf> audioObject;
    Windows::Foundation::Numerics::float3 position;
    Windows::Foundation::Numerics::float3 velocity;
    float yRotationRads;
    float deltaYRotation;
    float frequency; // in Hz
    UINT totalFrameCount;
};

동적 HRTF 오디오 개체에 대한 구현 단계는 이전 섹션에서 설명한 동적 오디오 개체의 단계와 거의 동일합니다. 먼저 오디오 엔드포인트를 가져옵니다.

HRESULT hr;
Microsoft::WRL::ComPtr<IMMDeviceEnumerator> deviceEnum;
Microsoft::WRL::ComPtr<IMMDevice> defaultDevice;

hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, __uuidof(IMMDeviceEnumerator), (void**)&deviceEnum);
hr = deviceEnum->GetDefaultAudioEndpoint(EDataFlow::eRender, eMultimedia, &defaultDevice);

다음으로, 공간 오디오 스트림을 초기화합니다. IMMDevice::Activate호출하여 ISpatialAudioClient 인스턴스를 가져옵니다. ISpatialAudioClient::IsAudioObjectFormatSupported 호출하여 사용 중인 오디오 형식이 지원되는지 확인합니다. 오디오 파이프라인이 더 많은 오디오 데이터에 대한 준비가 되었음을 앱에 알리는 데 사용할 이벤트를 만듭니다.

ISpatialAudioClient::GetMaxDynamicObjectCount 호출하여 시스템에서 지원하는 동적 개체의 수를 검색합니다. 이 호출이 0을 반환하면 동적 공간 오디오 개체가 현재 디바이스에서 지원되거나 활성화되지 않습니다. 공간 오디오를 사용하도록 설정하는 방법과 다양한 공간 오디오 형식에 사용할 수 있는 동적 오디오 개체의 수에 대한 자세한 내용은 공간 사운드 참조하세요.

SpatialAudioHrtfActivationParams 구조를 채울 때 MaxDynamicObjectCount 필드를 앱에서 사용할 최대 동적 개체 수로 설정합니다. HRTF에 대한 활성화 매개 변수는 몇 가지 추가 매개 변수를 지원합니다. SpatialAudioHrtfDistanceDecay, SpatialAudioHrtfDirectivityUnion, SpatialAudioHrtfEnvironmentType및 스트림에서 만든 새 개체에 대한 이러한 설정의 기본값을 지정하는 SpatialAudioHrtfOrientation등입니다. 이러한 매개 변수는 선택 사항입니다. 필드를 nullptr로 설정하여 기본값을 제공하지 않습니다.

ISpatialAudioClient::ActivateSpatialAudioStream 호출하여 스트림을 활성화합니다.

// Activate ISpatialAudioClient on the desired audio-device 
Microsoft::WRL::ComPtr<ISpatialAudioClient> spatialAudioClient;
hr = defaultDevice->Activate(__uuidof(ISpatialAudioClient), CLSCTX_INPROC_SERVER, nullptr, (void**)&spatialAudioClient);

Microsoft::WRL::ComPtr<ISpatialAudioObjectRenderStreamForHrtf>  spatialAudioStreamForHrtf;
hr = spatialAudioClient->IsSpatialAudioStreamAvailable(__uuidof(spatialAudioStreamForHrtf), NULL);

hr = spatialAudioClient->IsAudioObjectFormatSupported(&format);

// Create the event that will be used to signal the client for more data
HANDLE bufferCompletionEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);

UINT32 maxDynamicObjectCount;
hr = spatialAudioClient->GetMaxDynamicObjectCount(&maxDynamicObjectCount);

SpatialAudioHrtfActivationParams streamParams;
streamParams.ObjectFormat = &format;
streamParams.StaticObjectTypeMask = AudioObjectType_None;
streamParams.MinDynamicObjectCount = 0;
streamParams.MaxDynamicObjectCount = min(maxDynamicObjectCount, 4);
streamParams.Category = AudioCategory_GameEffects;
streamParams.EventHandle = bufferCompletionEvent;
streamParams.NotifyObject = NULL;

SpatialAudioHrtfDistanceDecay decayModel;
decayModel.CutoffDistance = 100;
decayModel.MaxGain = 3.98f;
decayModel.MinGain = float(1.58439 * pow(10, -5));
decayModel.Type = SpatialAudioHrtfDistanceDecayType::SpatialAudioHrtfDistanceDecay_NaturalDecay;
decayModel.UnityGainDistance = 1;

streamParams.DistanceDecay = &decayModel;

SpatialAudioHrtfDirectivity directivity;
directivity.Type = SpatialAudioHrtfDirectivityType::SpatialAudioHrtfDirectivity_Cone;
directivity.Scaling = 1.0f;

SpatialAudioHrtfDirectivityCone cone;
cone.directivity = directivity;
cone.InnerAngle = 0.1f;
cone.OuterAngle = 0.2f;

SpatialAudioHrtfDirectivityUnion directivityUnion;
directivityUnion.Cone = cone;
streamParams.Directivity = &directivityUnion;

SpatialAudioHrtfEnvironmentType environment = SpatialAudioHrtfEnvironmentType::SpatialAudioHrtfEnvironment_Large;
streamParams.Environment = &environment;

SpatialAudioHrtfOrientation orientation = { 1,0,0,0,1,0,0,0,1 }; // identity matrix
streamParams.Orientation = &orientation;

PROPVARIANT pv;
PropVariantInit(&pv);
pv.vt = VT_BLOB;
pv.blob.cbSize = sizeof(streamParams);
pv.blob.pBlobData = (BYTE *)&streamParams;

hr = spatialAudioClient->ActivateSpatialAudioStream(&pv, __uuidof(spatialAudioStreamForHrtf), (void**)&spatialAudioStreamForHrtf);

다음은 임의로 배치된 오디오 개체를 동적으로 생성하고 벡터에 저장하는 이 예제를 지원하는 데 필요한 일부 앱별 코드입니다.

// Used for generating a vector of randomized My3DObject structs
std::vector<My3dObjectForHrtf> objectVector;
std::default_random_engine gen;
std::uniform_real_distribution<> pos_dist(-10, 10); // uniform distribution for random position
std::uniform_real_distribution<> vel_dist(-.02, .02); // uniform distribution for random velocity
std::uniform_real_distribution<> yRotation_dist(-3.14, 3.14); // uniform distribution for y-axis rotation
std::uniform_real_distribution<> deltaYRotation_dist(.01, .02); // uniform distribution for y-axis rotation
std::uniform_real_distribution<> pitch_dist(40, 400); // uniform distribution for random pitch

int spawnCounter = 0;

오디오 렌더링 루프를 입력하기 전에 ISpatialAudioObjectRenderStreamForHrtf::Start 호출하여 미디어 파이프라인에 오디오 데이터 요청을 시작하도록 지시합니다.

렌더링 루프 내에서 공간 오디오 스트림이 신호를 받을 수 있도록 초기화되었을 때 제공한 버퍼 완성 이벤트를 기다립니다. 렌더링 유형 또는 엔드포인트가 변경되면 해당 이벤트가 신호를 받지 않으므로 이벤트를 대기할 때 100ms와 같은 적절한 시간 제한(예: 100ms)을 설정해야 합니다. 이 경우 ISpatialAudioRenderStreamForHrtf::Reset 호출하여 공간 오디오 스트림을 다시 설정할 수 있습니다.

다음으로, ISpatialAudioRenderStreamForHrtf::BeginUpdatingAudioObjects 호출하여 오디오 개체의 버퍼를 데이터로 채우려고 한다는 것을 시스템에 알릴 수 있습니다. 이 메서드는 이 예제에서 사용되지 않는 사용 가능한 동적 오디오 개체의 수와 이 스트림에서 렌더링된 오디오 개체에 대한 버퍼의 프레임 수를 반환합니다.

생성 카운터가 지정된 값에 도달할 때마다 ISpatialAudioRenderStreamForHrtf::ActivateSpatialAudioObjectForHrtf를 호출하여 AudioObjectType_Dynamic를 지정하고 새 동적 오디오 객체를 활성화합니다. 사용 가능한 모든 동적 오디오 개체가 이미 할당된 경우 이 메서드는 SPLAUDCLNT_E_NO_MORE_OBJECTS반환합니다. 이 경우 앱별 우선 순위에 따라 이전에 활성화된 오디오 개체를 하나 이상 해제하도록 선택할 수 있습니다. 동적 오디오 개체를 만든 후 임의 위치, 회전, 속도, 볼륨 및 빈도 값을 사용하여 새 My3dObjectForHrtf 구조에 추가된 다음 활성 개체 목록에 추가됩니다.

다음으로, 앱 정의 My3dObjectForHrtf 구조로 이 예제에서 보여지는 모든 활성 개체를 반복합니다. 각 오디오 개체에 대해 ISpatialAudioObjectForHrtf::GetBuffer 호출하여 공간 오디오 개체의 오디오 버퍼에 대한 포인터를 가져옵니다. 이 메서드는 버퍼의 크기(바이트)도 반환합니다. 이 문서의 앞에 나열된 writeToAudioObjectBuffer 도우미 메서드는 버퍼를 오디오 데이터로 채웁니다. 버퍼에 쓴 후 예제에서는 ISpatialAudioObjectForHrtf::SetPosition 호출하고 ISpatialAudioObjectForHrtf::SetOrientationHRTF 오디오 개체의 위치와 방향을 업데이트합니다. 이 예제에서는 3D 개체가 가리키는 방향에 따라 방향 행렬을 계산하는 데 calculateEmitterConeOrientationMatrix도우미 메서드를 사용합니다. 이 메서드의 구현은 다음과 같습니다. ISpatialAudioObjectForHrtf::SetGain호출하여 오디오 개체의 볼륨을 수정할 수도 있습니다. 개체의 위치, 방향 또는 볼륨을 업데이트하지 않으면 개체가 마지막으로 설정된 시점부터 위치, 방향 및 볼륨이 유지됩니다. 개체의 앱 정의 수명에 도달하면 iSpatialAudioObjectForHrtf::SetEndOfStream 이 호출되어 오디오 파이프라인에 이 개체를 사용하여 더 이상 오디오가 기록되지 않음을 알리고 개체가 리소스를 확보하기 위해 nullptr 로 설정됩니다.

모든 오디오 개체에 데이터를 쓴 후 ISpatialAudioRenderStreamForHrtf::EndUpdatingAudioObjects 호출하여 데이터가 렌더링할 준비가 되었는지 시스템에 알릴 수 있습니다. BeginUpdatingAudioObjects 와 EndUpdatingAudioObjects 사이에만 GetBuffer을 호출할 수 있습니다.

// Start streaming / rendering 
hr = spatialAudioStreamForHrtf->Start();

do
{
    // Wait for a signal from the audio-engine to start the next processing pass
    if (WaitForSingleObject(bufferCompletionEvent, 100) != WAIT_OBJECT_0)
    {
        break;
    }

    UINT32 availableDynamicObjectCount;
    UINT32 frameCount;

    // Begin the process of sending object data and metadata
    // Get the number of active objects that can be used to send object-data
    // Get the frame count that each buffer will be filled with 
    hr = spatialAudioStreamForHrtf->BeginUpdatingAudioObjects(&availableDynamicObjectCount, &frameCount);

    BYTE* buffer;
    UINT32 bufferLength;

    // Spawn a new dynamic audio object every 200 iterations
    if (spawnCounter % 200 == 0 && spawnCounter < 1000)
    {
        // Activate a new dynamic audio object
        Microsoft::WRL::ComPtr<ISpatialAudioObjectForHrtf> audioObject;
        hr = spatialAudioStreamForHrtf->ActivateSpatialAudioObjectForHrtf(AudioObjectType::AudioObjectType_Dynamic, &audioObject);

        // If SPTLAUDCLNT_E_NO_MORE_OBJECTS is returned, there are no more available objects
        if (SUCCEEDED(hr))
        {
            // Init new struct with the new audio object.
            My3dObjectForHrtf obj = { audioObject,
                Windows::Foundation::Numerics::float3(static_cast<float>(pos_dist(gen)), static_cast<float>(pos_dist(gen)), static_cast<float>(pos_dist(gen))),
                Windows::Foundation::Numerics::float3(static_cast<float>(vel_dist(gen)), static_cast<float>(vel_dist(gen)), static_cast<float>(vel_dist(gen))),
                static_cast<float>(static_cast<float>(yRotation_dist(gen))),
                static_cast<float>(static_cast<float>(deltaYRotation_dist(gen))),
                static_cast<float>(static_cast<float>(pitch_dist(gen))),
                format.nSamplesPerSec * 5 // 5 seconds of audio samples
            };

            objectVector.insert(objectVector.begin(), obj);
        }
    }
    spawnCounter++;

    // Loop through all dynamic audio objects
    std::vector<My3dObjectForHrtf>::iterator it = objectVector.begin();
    while (it != objectVector.end())
    {
        it->audioObject->GetBuffer(&buffer, &bufferLength);

        if (it->totalFrameCount >= frameCount)
        {
            // Write audio data to the buffer
            WriteToAudioObjectBuffer(reinterpret_cast<float*>(buffer), frameCount, it->frequency, format.nSamplesPerSec);

            // Update the position and volume of the audio object
            it->audioObject->SetPosition(it->position.x, it->position.y, it->position.z);
            it->position += it->velocity;


            Windows::Foundation::Numerics::float3 emitterDirection = Windows::Foundation::Numerics::float3(cos(it->yRotationRads), 0, sin(it->yRotationRads));
            Windows::Foundation::Numerics::float3 listenerDirection = Windows::Foundation::Numerics::float3(0, 0, 1);
            DirectX::XMFLOAT4X4 rotationMatrix;

            DirectX::XMMATRIX rotation = CalculateEmitterConeOrientationMatrix(emitterDirection, listenerDirection);
            XMStoreFloat4x4(&rotationMatrix, rotation);

            SpatialAudioHrtfOrientation orientation = {
                rotationMatrix._11, rotationMatrix._12, rotationMatrix._13,
                rotationMatrix._21, rotationMatrix._22, rotationMatrix._23,
                rotationMatrix._31, rotationMatrix._32, rotationMatrix._33
            };

            it->audioObject->SetOrientation(&orientation);
            it->yRotationRads += it->deltaYRotation;

            it->totalFrameCount -= frameCount;

            ++it;
        }
        else
        {
            // If the audio object reaches its lifetime, set EndOfStream and release the object

            // Write audio data to the buffer
            WriteToAudioObjectBuffer(reinterpret_cast<float*>(buffer), it->totalFrameCount, it->frequency, format.nSamplesPerSec);

            // Set end of stream for the last buffer 
            hr = it->audioObject->SetEndOfStream(it->totalFrameCount);

            it->audioObject = nullptr; // Release the object

            it->totalFrameCount = 0;

            it = objectVector.erase(it);
        }
    }

    // Let the audio-engine know that the object data are available for processing now
    hr = spatialAudioStreamForHrtf->EndUpdatingAudioObjects();

} while (objectVector.size() > 0);

공간 오디오 렌더링이 완료되면 ISpatialAudioRenderStreamForHrtf::Stop호출하여 공간 오디오 스트림을 중지합니다. 스트림을 다시 사용하지 않을 경우 ISpatialAudioRenderStreamForHrtf::Reset호출하여 리소스를 해제합니다.

// Stop the stream 
hr = spatialAudioStreamForHrtf->Stop();

// We don't want to start again, so reset the stream to free it's resources.
hr = spatialAudioStreamForHrtf->Reset();

CloseHandle(bufferCompletionEvent);

다음 코드 예제에서는 3D 개체가 가리키는 방향에 따라 방향 행렬을 계산하기 위해 위의 예제에서 사용된 CalculateEmitterConeOrientationMatrix 도우미 메서드의 구현을 보여줍니다.

DirectX::XMMATRIX CalculateEmitterConeOrientationMatrix(Windows::Foundation::Numerics::float3 listenerOrientationFront, Windows::Foundation::Numerics::float3 emitterDirection)
{
    DirectX::XMVECTOR vListenerDirection = DirectX::XMLoadFloat3(&listenerOrientationFront);
    DirectX::XMVECTOR vEmitterDirection = DirectX::XMLoadFloat3(&emitterDirection);
    DirectX::XMVECTOR vCross = DirectX::XMVector3Cross(vListenerDirection, vEmitterDirection);
    DirectX::XMVECTOR vDot = DirectX::XMVector3Dot(vListenerDirection, vEmitterDirection);
    DirectX::XMVECTOR vAngle = DirectX::XMVectorACos(vDot);
    float angle = DirectX::XMVectorGetX(vAngle);

    // The angle must be non-zero
    if (fabsf(angle) > FLT_EPSILON)
    {
        // And less than PI
        if (fabsf(angle) < DirectX::XM_PI)
        {
            return DirectX::XMMatrixRotationAxis(vCross, angle);
        }

        // If equal to PI, find any other non-collinear vector to generate the perpendicular vector to rotate about
        else
        {
            DirectX::XMFLOAT3 vector = { 1.0f, 1.0f, 1.0f };
            if (listenerOrientationFront.x != 0.0f)
            {
                vector.x = -listenerOrientationFront.x;
            }
            else if (listenerOrientationFront.y != 0.0f)
            {
                vector.y = -listenerOrientationFront.y;
            }
            else // if (_listenerOrientationFront.z != 0.0f)
            {
                vector.z = -listenerOrientationFront.z;
            }
            DirectX::XMVECTOR vVector = DirectX::XMLoadFloat3(&vector);
            vVector = DirectX::XMVector3Normalize(vVector);
            vCross = DirectX::XMVector3Cross(vVector, vEmitterDirection);
            return DirectX::XMMatrixRotationAxis(vCross, angle);
        }
    }

    // If the angle is zero, use an identity matrix
    return DirectX::XMMatrixIdentity();
}

공간 소리

ISpatialAudioClient

ISpatialAudioObject