共用方式為


使用空間音頻物件轉譯空間音效

本文提供一些簡單的範例,說明如何使用靜態空間音頻對象、動態空間音訊物件,以及使用 Microsoft 前端相對傳輸函數的空間音訊物件 (HRTF) 實作空間音效。 這三種技術的實作步驟非常類似,本文會針對每個技術提供類似結構化的程式代碼範例。 如需真實世界空間音頻實作的完整端對端範例,請參閱 Microsoft Spatial Sound 範例 github 存放庫。 如需 Windows Sonic 的概觀,Microsoft 適用於 Xbox 和 Windows 上空間音效支援的平臺層級解決方案,請參閱 空間音效

使用靜態空間音頻物件轉譯音訊

靜態音頻物件可用來將音效轉譯為 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實例,並呼叫 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);

當您建立空間音訊串流時,您必須指定數據流將使用的音訊格式,方法是提供 「電壓ATEX 」結構。 如果您要從檔案播放音訊,格式通常是由音訊檔格式決定。 此範例使用mono、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 介面時,您必須先呼叫 EnableSpatialAudio,再呼叫 IMMDeviceEnumerator::EnumAudioEndpoints IMMDeviceEnumerator::GetDefaultAudioEndpoint。 若無法這麼做,會導致從呼叫 Activate 傳回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秒後停止轉譯音訊。

在轉譯迴圈內,等候初始化空間音訊數據流時提供的緩衝區完成事件,以發出訊號。 您應該在等候事件時設定合理的逾時限制,例如 100 毫秒,因為轉譯類型或端點的任何變更都會導致該事件永遠不會收到訊號。 在此情況下,您可以呼叫 ISpatialAudioObjectRenderStream::Reset 來嘗試重設空間音頻數據流。

接下來,呼叫 ISpatialAudioObjectRenderStream::BeginUpdatingAudioObjects ,讓系統知道您即將使用數據填滿音訊對象的緩衝區。 這個方法會傳回這個範例中未使用的可用動態音訊對象數目,以及此數據流所轉譯之音訊對象的緩衝區畫面計數。

如果尚未建立靜態空間音頻物件,請呼叫 ISpatialAudioObjectRenderStream::ActivateSpatialAudioObject 來建立一或多個物件,並傳入 AudioObjectType 列舉中的值,指出物件的音頻轉譯至靜態通道。

接下來,呼叫 ISpatialAudioObject::GetBuffer 以取得空間音訊物件音訊緩衝區的指標。 這個方法也會傳回緩衝區的大小,以位元組為單位。 此範例會使用 Helper 方法 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 以指示媒體管線開始要求音頻數據。

在轉譯迴圈內,等候我們在初始化空間音頻數據流以發出訊號時提供的緩衝區完成事件。 您應該在等候事件時設定合理的逾時限制,例如 100 毫秒,因為轉譯類型或端點的任何變更都會導致該事件永遠不會收到訊號。 在此情況下,您可以呼叫 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 ISpatialAudioRenderStreamForHrtf ISpatialAudioObjectForHrtf,可讓空間音訊使用 Microsoft 的前端相對傳輸函式 (HRTF) 來衰減音效,以仿真相對於使用者的空間中發出器的位置,而使用者可能會隨著時間而變更。 除了位置之外,HRTF 音訊物件還可讓您在空間中指定方向、發出聲音的直接性,例如圓錐形或心體形狀,以及隨著對象在虛擬接聽程式附近和更遠移動的衰變模型。 請注意,只有在用戶選取 耳機用 Windows Sonic 作為裝置的空間音訊引擎時,才能使用這些 HRTF 介面。 如需設定裝置以使用 耳機用 Windows Sonic 的資訊,請參閱空間音效

ISpatialAudioRenderStreamForHrtf 和 ISpatialAudioObjectForHrtf API 可讓應用程式直接使用 耳機用 Windows Sonic 轉譯路徑。 這些 API 不支援空間音效格式,例如 Dolby Atmos for Home Theater 或 Dolby Atmos for Headphones,或透過 聲音 控制面板切換消費者控制的輸出格式,或透過喇叭播放。 這些介面適用於 Windows Mixed Reality 應用程式中,這些應用程式想要使用 耳機用 Windows Sonic 特定功能(例如以程式設計方式指定的環境預設值和以距離為基礎的變換,在一般內容撰寫管線之外)。 大部分的遊戲和虛擬實境案例都偏好改用 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、SpatialAudioHrtfDirectivityUnionSpatialAudioHrtfEnvironmentType 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 以指示媒體管線開始要求音頻數據。

在轉譯迴圈內,等候我們在初始化空間音頻數據流以發出訊號時提供的緩衝區完成事件。 您應該在等候事件時設定合理的逾時限制,例如 100 毫秒,因為轉譯類型或端點的任何變更都會導致該事件永遠不會收到訊號。 在此情況下,您可以呼叫 ISpatialAudioRenderStreamForHrtf::Reset 來嘗試重設空間音訊數據流。

接下來,呼叫 ISpatialAudioRenderStreamForHrtf::BeginUpdatingAudioObjects ,讓系統知道您即將使用數據填滿音頻對象的緩衝區。 這個方法會傳回這個範例中未使用的可用動態音訊對象數目,以及此數據流所轉譯之音訊對象的緩衝區畫面計數。

每當繁衍計數器到達指定的值時,我們會呼叫 ISpatialAudioRenderStreamForHrtf::ActivateSpatialAudioObjectForHrtf AudioObjectType_Dynamic啟動新的動態音頻物件。 如果已配置所有可用的動態音訊對象,這個方法會傳回 SPLAUDCLNT_E_NO_MORE_OBJECTS。 在此情況下,您可以選擇根據應用程式特定的優先順序釋放一或多個先前啟動的音訊物件。 建立動態音頻對象之後,它會新增至新的 My3dObjectForHrtf 結構,其中包含隨機位置、旋轉、速度、音量和頻率值,然後新增至使用中物件清單。

接下來,使用應用程式定義的 My3dObjectForHrtf 結構,逐一查看此範例中代表的所有使用中物件。 針對每個音頻物件,呼叫 ISpatialAudioObjectForHrtf::GetBuffer 以取得空間音頻物件的音頻緩衝區指標。 這個方法也會傳回緩衝區的大小,以位元組為單位。 協助程式方法 WriteToAudioObjectBuffer,先前列於本文中,以填滿緩衝區的音訊數據。 寫入緩衝區之後,此範例會呼叫 ISpatialAudioObjectForHrtf::SetPosition 和 ISpatialAudioObjectForHrtf::SetOrientation 來更新 HRTF 音頻物件的位置和方向。 在此範例中,會使用 Helper 方法 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);

下列程式代碼範例顯示 CalculateEmitterConeOrientationMatrix 協助程式方法的實作,這在上述範例中用來計算 3D 物件指向的方向矩陣。

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