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

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

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

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

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

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의 instance 만들고 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의 instance 가져옵니다. 사용 중인 오디오 형식이 지원되는지 확인하려면 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와 같은 적절한 시간 제한(예: )을 설정해야 합니다. 이 경우 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 도우미 메서드는 샘플의 전체 버퍼 또는 앱에서 정의한 시간 제한에 지정된 나머지 샘플 수를 씁니다. 예를 들어 원본 오디오 파일에 남아 있는 샘플 수에 따라 확인할 수도 있습니다. 빈도 입력 매개 변수에 의해 크기가 조정되는 단순 죄파가 생성되어 버퍼에 기록됩니다.

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의 instance 가져옵니다. 사용 중인 오디오 형식이 지원되는지 확인하려면 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와 같은 적절한 시간 제한(예: )을 설정해야 합니다. 이 경우 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 를 호출하여 데이터가 렌더링할 준비가 되었는지 시스템에 알릴 수 있습니다. BeginUpdatingAudioObjects와 EndUpdatingAudioObjects 호출 사이에만 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(헤드 상대 전송 함수)를 사용하여 소리를 감쇠하여 시간에 따라 변경할 수 있는 사용자에 비해 공간에서 발광자의 위치를 시뮬레이션하는 공간 오디오를 사용하도록 설정합니다. 위치 외에도 HRTF 오디오 개체를 사용하면 공간의 방향, 원뿔형 또는 카디오이드 모양과 같이 소리가 내보내지는 직접성 및 개체가 가상 수신기에서 더 가깝고 더 멀리 이동할 때 감쇠 모델을 지정할 수 있습니다. 이러한 HRTF 인터페이스는 사용자가 디바이스의 공간 오디오 엔진으로 헤드폰용 Windows Sonic 선택한 경우에만 사용할 수 있습니다. 헤드폰용 Windows Sonic 사용하도록 디바이스를 구성하는 방법에 대한 자세한 내용은 공간 사운드를 참조하세요.

ISpatialAudioRenderStreamForHrtfISpatialAudioObjectForHrtf API를 사용하면 애플리케이션이 헤드폰용 Windows Sonic 렌더링 경로를 직접 명시적으로 사용할 수 있습니다. 이러한 API는 Dolby Atmos for Home Theater 또는 Dolby Atmos for Headphones 같은 공간 사운드 형식이나 사운드 제어판을 통한 소비자 제어 출력 형식 전환 또는 스피커를 통한 재생을 지원하지 않습니다. 이러한 인터페이스는 헤드폰용 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의 instance 가져옵니다. 사용 중인 오디오 형식이 지원되는지 확인하려면 ISpatialAudioClient::IsAudioObjectFormatSupported 를 호출합니다. 오디오 파이프라인이 더 많은 오디오 데이터를 사용할 준비가 되었음을 앱에 알리는 데 사용할 이벤트를 만듭니다.

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

SpatialAudioHrtfActivationParams 구조를 채울 때 MaxDynamicObjectCount 필드를 앱에서 사용할 최대 동적 개체 수로 설정합니다. HRTF에 대한 활성화 매개 변수는 SpatialAudioHrtfDistanceDecay, SpatialAudioHrtfDirectivityUnion, SpatialAudioHrtfEnvironmentType 및 스트림에서 만든 새 개체에 대한 이러한 설정의 기본값을 지정하는 SpatialAudioHrtfHrtfOrientation과 같은 몇 가지 추가 매개 변수를 지원합니다. 이러한 매개 변수는 선택 사항입니다. 필드를 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와 같은 적절한 시간 제한(예: )을 설정해야 합니다. 이 경우 ISpatialAudioRenderStreamForHrtf::Reset 를 호출하여 공간 오디오 스트림을 다시 설정하려고 시도할 수 있습니다.

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

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

다음으로, 앱 정의 My3dObjectForHrtf 구조체를 사용하여 이 예제에 표시된 모든 활성 개체를 반복합니다. 각 오디오 개체에 대해 ISpatialAudioObjectForHrtf::GetBuffer 를 호출하여 공간 오디오 개체의 오디오 버퍼에 대한 포인터를 가져옵니다. 또한 이 메서드는 버퍼의 크기(바이트)를 반환합니다. 이 문서의 앞에 나열된 도우미 메서드인 WriteToAudioObjectBuffer는 버퍼를 오디오 데이터로 채웁니다. 버퍼에 쓴 후 이 예제에서는 ISpatialAudioObjectForHrtf::SetPositionISpatialAudioObjectForHrtf::SetOrientation을 호출하여 HRTF 오디오 개체의 위치와 방향을 업데이트합니다. 이 예제에서는 도우미 메서드인 CalculateEmitterConeOrientationMatrix를 사용하여 3D 개체가 가리키는 방향에 따라 방향 행렬을 계산합니다. 이 메서드의 구현은 다음과 같습니다. 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