Bagikan melalui


Merender Suara Spasial Menggunakan Objek Audio Spasial

Artikel ini menyajikan beberapa contoh sederhana yang menggambarkan cara mengimplementasikan suara spasial menggunakan objek audio spasial statis, objek audio spasial dinamis, dan objek audio spasial yang menggunakan Fungsi Transfer Relatif Kepala (HRTF) Microsoft. Langkah-langkah implementasi untuk ketiga teknik ini sangat mirip dan artikel ini memberikan contoh kode terstruktur yang sama untuk setiap teknik. Untuk contoh lengkap implementasi audio spasial dunia nyata, lihat repositori github sampel Microsoft Spatial Sound. Untuk gambaran umum Windows Sonic, solusi tingkat platform Microsoft untuk dukungan suara spasial di Xbox dan Windows, lihat Spatial Sound.

Merender audio menggunakan objek audio spasial statis

Objek audio statis digunakan untuk merender suara ke salah satu dari 18 saluran audio statis yang ditentukan dalam enumerasi AudioObjectType. Masing-masing saluran ini mewakili speaker nyata atau virtual pada titik tetap di ruang yang tidak bergerak dari waktu ke waktu. Saluran statis yang tersedia pada perangkat tertentu bergantung pada format suara spasial yang digunakan. Untuk daftar format yang didukung dan jumlah salurannya, lihat Suara Spasial.

Saat menginisialisasi aliran audio spasial, Anda harus menentukan saluran statis mana yang tersedia yang akan digunakan aliran. Contoh definisi konstanta berikut dapat digunakan untuk menentukan konfigurasi speaker umum dan mendapatkan jumlah saluran yang tersedia untuk masing-masing.

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);

Langkah pertama dalam merender audio spasial adalah mendapatkan titik akhir audio tempat data audio akan dikirim. Buat instans MMDeviceEnumerator dan panggil GetDefaultAudioEndpoint untuk mendapatkan perangkat render audio default.

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);

Saat membuat aliran audio spasial, Anda harus menentukan format audio yang akan digunakan aliran dengan menyediakan struktur WAVEFORMATEX. Jika Anda memutar kembali audio dari file, format biasanya ditentukan oleh format file audio. Contoh ini menggunakan format mono, 32-bit, 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;

Langkah selanjutnya dalam merender audio spasial adalah menginisialisasi aliran audio spasial. Pertama, dapatkan instans ISpatialAudioClient dengan memanggil IMMDevice::Activate. Panggil ISpatialAudioClient::IsAudioObjectFormatSupported untuk memastikan bahwa format audio yang Anda gunakan didukung. Buat peristiwa yang akan digunakan alur audio untuk memberi tahu aplikasi bahwa alur audio siap untuk data audio tambahan.

Isi struktur SpatialAudioObjectRenderStreamActivationParams yang akan digunakan untuk menginisialisasi aliran audio spasial. Dalam contoh ini, bidang StaticObjectTypeMask ditetapkan ke konstanta ChannelMask_Stereo seperti yang sudah ditentukan sebelumnya dalam artikel ini, yang berarti bahwa hanya saluran kanan depan dan kiri yang dapat digunakan oleh aliran audio. Karena contoh ini hanya menggunakan objek audio statis dan tidak ada objek dinamis, bidang MaxDynamicObjectCount diatur ke 0. Bidang Kategori disetel pada anggota AUDIO_STREAM_CATEGORY enumerasi, yang mengatur bagaimana sistem mencampurkan suara dari stream ini dengan sumber audio lainnya.

Panggil ISpatialAudioClient::ActivateSpatialAudioStream untuk mengaktifkan aliran.

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);

Nota

Saat menggunakan antarmukaISpatialAudioClient pada judul Xbox One Development Kit (XDK), Anda harus terlebih dahulu memanggil EnableSpatialAudio sebelum memanggil IMMDeviceEnumerator::EnumAudioEndpoints atau IMMDeviceEnumerator::GetDefaultAudioEndpoint. Kegagalan untuk melakukannya akan mengakibatkan kesalahan E_NOINTERFACE dikembalikan dari panggilan ke Aktifkan. EnableSpatialAudio hanya tersedia untuk judul XDK, dan tidak perlu dipanggil untuk aplikasi Universal Windows Platform yang berjalan di Xbox One, atau untuk perangkat non-Xbox One.

 

Mendeklarasikan pointer untuk ISpatialAudioObject yang akan digunakan untuk menulis data audio ke saluran statis. Aplikasi pada umumnya akan menggunakan objek untuk setiap saluran yang ditentukan di bidang StaticObjectTypeMask. Untuk kesederhanaan, contoh ini hanya menggunakan satu objek audio statis.

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

Sebelum memasuki proses rendering audio, panggil ISpatialAudioObjectRenderStream::Start untuk menginstruksikan jalur media untuk mulai meminta data audio. Contoh ini menggunakan penghitung untuk menghentikan penyajian audio setelah 5 detik.

Di dalam loop render, tunggu hingga peristiwa penyelesaian buffer, yang diberikan saat aliran audio spasial diinisialisasi, mendapatkan sinyal. Anda harus menetapkan batas waktu habis yang wajar, seperti 100 ms, saat menunggu peristiwa karena perubahan apa pun pada jenis render atau titik akhir akan menyebabkan peristiwa tersebut tidak pernah diberi sinyal. Dalam hal ini, Anda dapat memanggil ISpatialAudioObjectRenderStream::Reset untuk mencoba mengatur ulang aliran audio spasial.

Selanjutnya, panggil ISpatialAudioObjectRenderStream::BeginUpdatingAudioObjects untuk memberi tahu sistem bahwa Anda akan mengisi buffer objek audio dengan data. Metode ini mengembalikan jumlah objek audio dinamis yang tersedia, tidak digunakan dalam contoh ini, dan jumlah bingkai buffer untuk objek audio yang dirender oleh aliran ini.

Jika objek audio spasial statis belum dibuat, buat satu atau beberapa dengan memanggil ISpatialAudioObjectRenderStream::ActivateSpatialAudioObject, meneruskan nilai dari AudioObjectType enumerasi yang menunjukkan saluran statis tempat audio objek dirender.

Selanjutnya, panggil ISpatialAudioObject::GetBuffer untuk mendapatkan penunjuk ke buffer audio objek audio spasial. Metode ini juga mengembalikan ukuran buffer, dalam byte. Contoh ini menggunakan metode pembantu, WriteToAudioObjectBuffer, untuk mengisi buffer dengan data audio. Metode ini ditampilkan nanti dalam artikel ini. Setelah menulis ke buffer, contoh memeriksa untuk melihat apakah masa pakai 5 detik objek telah tercapai, dan jika demikian, ISpatialAudioObject::SetEndOfStream dipanggil untuk memberi tahu alur audio bahwa tidak ada lagi audio yang akan ditulis menggunakan objek ini dan objek diatur ke nullptr untuk membebaskan sumber dayanya.

Setelah menulis data ke semua objek audio Anda, panggil ISpatialAudioObjectRenderStream::EndUpdatingAudioObjects untuk memberi tahu sistem bahwa data siap untuk dirender. Anda hanya dapat memanggil GetBuffer di antara panggilan ke BeginUpdatingAudioObjects dan EndUpdatingAudioObjects.

// 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();
};

Setelah selesai merender audio spasial, hentikan aliran audio spasial dengan memanggil ISpatialAudioObjectRenderStream::Stop. Jika Anda tidak akan menggunakan aliran lagi, bebaskan sumber dayanya dengan memanggil 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);

Metode bantuan WriteToAudioObjectBuffer menulis satu buffer penuh sampel atau jumlah sampel yang tersisa yang ditetapkan oleh batas waktu aplikasi kami. Ini juga dapat ditentukan, misalnya, dengan jumlah sampel yang tersisa dalam file audio sumber. Gelombang sinus sederhana, yang frekuensinya diskalakan oleh parameter input frekuensi , dihasilkan dan ditulis ke buffer.

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;
        }
    }
}

Merender audio menggunakan objek audio spasial dinamis

Objek dinamis memungkinkan Anda merender audio dari posisi arbitrer di ruang, relatif terhadap pengguna. Posisi dan volume objek audio dinamis dapat diubah dari waktu ke waktu. Game biasanya akan menggunakan posisi objek 3D di dunia game untuk menentukan posisi objek audio dinamis yang terkait dengannya. Contoh berikut akan menggunakan struktur sederhana, My3dObject, untuk menyimpan kumpulan data minimum yang diperlukan untuk mewakili objek. Data ini mencakup penunjuk ke ISpatialAudioObject, serta posisi, kecepatan, volume, dan frekuensi nada untuk objek tersebut, serta nilai yang menyimpan jumlah total bingkai di mana objek tersebut harus merender suara.

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;
};

Langkah-langkah implementasi untuk objek audio dinamis sebagian besar sama dengan langkah-langkah untuk objek audio statis yang dijelaskan di atas. Pertama, dapatkan titik akhir audio.

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);

Selanjutnya, inisialisasi aliran audio spasial. Dapatkan instans ISpatialAudioClient dengan memanggil IMMDevice::Activate. Panggil ISpatialAudioClient::IsAudioObjectFormatSupported untuk memastikan bahwa format audio yang Anda gunakan didukung. Buat event yang akan digunakan alur audio untuk memberitahu aplikasi bahwa alur audio siap untuk menerima lebih banyak data audio.

Panggil ISpatialAudioClient::GetMaxDynamicObjectCount untuk mengambil jumlah objek dinamis yang didukung oleh sistem. Jika panggilan ini mengembalikan 0, maka objek audio spasial dinamis tidak didukung atau diaktifkan pada perangkat saat ini. Untuk informasi tentang mengaktifkan audio spasial dan untuk detail tentang jumlah objek audio dinamis yang tersedia untuk format audio spasial yang berbeda, lihat Suara Spasial.

Saat mengisi strukturSpatialAudioObjectRenderStreamActivationParams, atur bidang MaxDynamicObjectCount ke jumlah maksimum objek dinamis yang akan digunakan aplikasi Anda.

Panggil ISpatialAudioClient::ActivateSpatialAudioStream untuk mengaktifkan aliran.

// 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);

Berikut ini adalah beberapa kode spesifik aplikasi yang diperlukan untuk mendukung contoh ini, yang akan secara dinamis menghasilkan objek audio yang diposisikan secara acak dan menyimpannya di dalam vektor.

// 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;

Sebelum memasuki perulangan render audio, panggil ISpatialAudioObjectRenderStream::Start untuk menginstruksikan jalur media untuk mulai meminta data audio.

Di dalam perulangan render, tunggu peristiwa penyelesaian buffer yang kami sediakan ketika aliran audio spasial diinisialisasi untuk diberi sinyal. Anda harus menetapkan batas waktu habis yang wajar, seperti 100 ms, saat menunggu peristiwa karena perubahan apa pun pada jenis render atau titik akhir akan menyebabkan peristiwa tersebut tidak pernah diberi sinyal. Dalam hal ini, Anda dapat memanggil ISpatialAudioObjectRenderStream::Reset untuk mencoba mengatur ulang aliran audio spasial.

Selanjutnya, panggil ISpatialAudioObjectRenderStream::BeginUpdatingAudioObjects untuk memberi tahu sistem bahwa Anda akan mengisi buffer objek audio dengan data. Metode ini mengembalikan jumlah objek audio dinamis yang tersedia dan jumlah bingkai buffer untuk objek audio yang dirender oleh aliran ini.

Setiap kali penghitung spawn mencapai nilai yang ditentukan, kami akan mengaktifkan objek audio dinamis baru dengan memanggil ISpatialAudioObjectRenderStream::ActivateSpatialAudioObject menentukan AudioObjectType_Dynamic. Jika semua objek audio dinamis yang tersedia telah dialokasikan, metode ini akan mengembalikan SPLAUDCLNT_E_NO_MORE_OBJECTS. Dalam hal ini, Anda dapat memilih untuk merilis satu atau beberapa objek audio yang diaktifkan sebelumnya berdasarkan prioritas khusus aplikasi Anda. Setelah objek audio dinamis dibuat, objek tersebut ditambahkan ke struktur My3dObject baru, dengan nilai posisi, kecepatan, volume, dan frekuensi acak, yang kemudian ditambahkan ke daftar objek aktif.

Selanjutnya, iterasikan semua objek aktif, yang diwakili dalam contoh ini dengan struktur My3dObject yang ditentukan oleh aplikasi. Untuk setiap objek audio, panggil ISpatialAudioObject::GetBuffer untuk mendapatkan penunjuk ke buffer audio objek audio spasial. Metode ini juga mengembalikan ukuran buffer, dalam byte. Metode pembantu, WriteToAudioObjectBuffer, untuk mengisi buffer dengan data audio. Setelah menulis ke buffer, contoh memperbarui posisi objek audio dinamis dengan memanggil ISpatialAudioObject::SetPosition. Volume objek audio juga dapat dimodifikasi dengan memanggil SetVolume. Jika Anda tidak memperbarui posisi atau volume objek, itu akan mempertahankan posisi dan volume dari terakhir kali ditetapkan. Jika masa pakai objek yang ditentukan aplikasi telah tercapai, ISpatialAudioObject::SetEndOfStream dipanggil untuk memberi tahu alur audio bahwa tidak ada lagi audio yang akan ditulis menggunakan objek ini dan objek diatur ke nullptr untuk membebaskan sumber dayanya.

Setelah menulis data ke semua objek audio Anda, panggil ISpatialAudioObjectRenderStream::EndUpdatingAudioObjects untuk memberi tahu sistem bahwa data siap untuk dirender. Anda hanya dapat memanggil GetBuffer di antara panggilan ke BeginUpdatingAudioObjects dan EndUpdatingAudioObjects.

// 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);

Setelah selesai merender audio spasial, hentikan aliran audio spasial dengan memanggil ISpatialAudioObjectRenderStream::Stop. Jika Anda tidak akan menggunakan aliran lagi, bebaskan sumber dayanya dengan memanggil 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);

Merender audio menggunakan objek audio spasial dinamis untuk HRTF

Set API lain, ISpatialAudioRenderStreamForHrtf dan ISpatialAudioObjectForHrtf, memungkinkan audio spasial yang menggunakan Fungsi Transfer Head-relative (HRTF) Microsoft untuk melemahkan suara guna mensimulasikan posisi pemancar di ruang, relatif terhadap pengguna, yang dapat diubah seiring waktu. Selain posisi, objek audio HRTF memungkinkan Anda menentukan orientasi di ruang, direktivitas dari mana suara dipancarkan, seperti bentuk kerucut atau kardioid, dan model peluruhan saat objek bergerak mendekat dan menjauh dari pendengar virtual. Perhatikan bahwa antarmuka HRTF ini hanya tersedia ketika pengguna telah memilih Windows Sonic untuk Headphone sebagai mesin audio spasial untuk perangkat. Untuk informasi tentang cara mengonfigurasi perangkat agar menggunakan Windows Sonic for Headphones, lihat Suara Spasial.

API ISpatialAudioRenderStreamForHrtf dan ISpatialAudioObjectForHrtf memungkinkan aplikasi untuk secara eksplisit menggunakan jalur render Windows Sonic for Headphones secara langsung. API ini tidak mendukung format suara spasial seperti Dolby Atmos untuk Home Theater atau Dolby Atmos untuk Headphone, atau peralihan format output yang dikontrol konsumen melalui panel kontrol Suara, atau pemutaran melalui speaker. Antarmuka ini ditujukan untuk digunakan dalam aplikasi Windows Mixed Reality yang ingin menggunakan kemampuan khusus Windows Sonic for Headphones (seperti prasetel lingkungan dan rolloff berbasis jarak yang ditentukan secara terprogram, di luar alur penulisan konten umum). Sebagian besar game dan skenario realitas virtual akan lebih suka menggunakan ISpatialAudioClient sebagai gantinya. Langkah-langkah implementasi untuk kedua set API hampir identik, sehingga dimungkinkan untuk mengimplementasikan teknologi dan beralih pada runtime tergantung pada fitur mana yang tersedia pada perangkat saat ini.

Aplikasi mixed-reality biasanya akan menggunakan posisi objek 3D di dunia virtual untuk menentukan posisi objek audio dinamis yang terkait dengannya. Contoh berikut akan menggunakan struktur sederhana, My3dObjectForHrtf, untuk menyimpan kumpulan data minimum yang diperlukan untuk mewakili objek. Data ini mencakup penunjuk ke ISpatialAudioObjectForHrtf, posisi, orientasi, kecepatan, dan frekuensi nada untuk objek, serta nilai yang menyimpan jumlah total frame yang diperlukan agar objek dapat merender suara.

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;
};

Langkah-langkah implementasi untuk objek audio HRTF dinamis sebagian besar sama dengan langkah-langkah untuk objek audio dinamis yang dijelaskan di bagian sebelumnya. Pertama, dapatkan titik akhir audio.

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);

Selanjutnya, inisialisasi aliran audio spasial. Dapatkan instance ISpatialAudioClient dengan memanggil IMMDevice::Activate. Panggil ISpatialAudioClient::IsAudioObjectFormatSupported untuk memastikan bahwa format audio yang Anda gunakan didukung. Buat sebuah acara yang akan digunakan oleh saluran audio untuk memberi tahu aplikasi bahwa saluran tersebut siap untuk data audio lebih lanjut.

Panggil ISpatialAudioClient::GetMaxDynamicObjectCount untuk mengambil jumlah objek dinamis yang didukung oleh sistem. Jika panggilan ini mengembalikan 0, maka objek audio spasial dinamis tidak didukung atau diaktifkan pada perangkat saat ini. Untuk informasi tentang mengaktifkan audio spasial dan untuk detail tentang jumlah objek audio dinamis yang tersedia untuk format audio spasial yang berbeda, lihat Suara Spasial.

Saat mengisi strukturSpatialAudioHrtfActivationParams, atur bidang MaxDynamicObjectCount ke jumlah maksimum objek dinamis yang akan digunakan aplikasi Anda. Param aktivasi untuk HRTF mendukung beberapa parameter tambahan, seperti SpatialAudioHrtfDistanceDecay, SpatialAudioHrtfDirectivityUnion, SpatialAudioHrtfEnvironmentType, dan SpatialAudioHrtfOrientation, yang menentukan nilai default pengaturan ini untuk objek baru yang dibuat dari aliran. Parameter ini bersifat opsional. Atur bidang ke nullptr untuk tidak memberikan nilai default.

Panggil ISpatialAudioClient::ActivateSpatialAudioStream untuk mengaktifkan aliran.

// 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);

Berikut ini adalah beberapa kode khusus aplikasi yang diperlukan untuk mendukung contoh ini, yang akan secara dinamis menghasilkan objek audio yang diposisikan secara acak dan menyimpannya di vektor.

// 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;

Sebelum memasukkan perulangan render audio, panggil ISpatialAudioObjectRenderStreamForHrtf::Start untuk menginstruksikan alur media untuk mulai meminta data audio.

Di dalam perulangan render, tunggu peristiwa penyelesaian buffer yang kami sediakan ketika aliran audio spasial diinisialisasi untuk diberi sinyal. Anda harus menetapkan batas waktu habis yang wajar, seperti 100 ms, saat menunggu peristiwa karena perubahan apa pun pada jenis render atau titik akhir akan menyebabkan peristiwa tersebut tidak pernah diberi sinyal. Dalam hal ini, Anda dapat memanggil ISpatialAudioRenderStreamForHrtf::Reset untuk mencoba mengatur ulang aliran audio spasial.

Selanjutnya, panggil ISpatialAudioRenderStreamForHrtf::BeginUpdatingAudioObjects untuk memberi tahu sistem bahwa Anda akan mengisi buffer objek audio dengan data. Metode ini mengembalikan jumlah objek audio dinamis yang tersedia, tidak digunakan dalam contoh ini, dan jumlah bingkai buffer untuk objek audio yang dirender oleh aliran ini.

Setiap kali penghitung spawn mencapai nilai yang ditentukan, kami akan mengaktifkan objek audio dinamis baru dengan memanggil ISpatialAudioRenderStreamForHrtf::ActivateSpatialAudioObjectForHrtf menentukan AudioObjectType_Dynamic. Jika semua objek audio dinamis yang tersedia telah dialokasikan, metode ini akan mengembalikan SPLAUDCLNT_E_NO_MORE_OBJECTS. Dalam hal ini, Anda dapat memilih untuk merilis satu atau beberapa objek audio yang diaktifkan sebelumnya berdasarkan prioritas khusus aplikasi Anda. Setelah objek audio dinamis dibuat, objek tersebut ditambahkan ke struktur My3dObjectForHrtf baru, dengan posisi acak, rotasi, kecepatan, volume, dan nilai frekuensi, yang kemudian ditambahkan ke daftar objek aktif.

Selanjutnya, ulangi semua objek aktif, yang diwakili dalam contoh ini dengan struktur My3dObjectForHrtf yang ditentukan aplikasi. Untuk setiap objek audio, panggil ISpatialAudioObjectForHrtf::GetBuffer untuk mendapatkan penunjuk ke buffer audio objek audio spasial. Metode ini juga mengembalikan ukuran buffer, dalam byte. Metode pembantu, WriteToAudioObjectBuffer, tercantum sebelumnya dalam artikel ini, untuk mengisi buffer dengan data audio. Setelah menulis ke buffer, contoh memperbarui posisi dan orientasi objek audio HRTF dengan memanggil ISpatialAudioObjectForHrtf::SetPosition dan ISpatialAudioObjectForHrtf::SetOrientation. Dalam contoh ini, metode pembantu, CalculateEmitterConeOrientationMatrix, digunakan untuk menghitung matriks orientasi mengingat arah objek 3D menunjuk. Implementasi metode ini ditunjukkan di bawah ini. Volume objek audio juga dapat dimodifikasi dengan memanggil ISpatialAudioObjectForHrtf::SetGain. Jika Anda tidak memperbarui posisi, orientasi, atau volume objek, posisi, orientasi, dan volume akan dipertahankan sejak terakhir kali ditetapkan. Jika masa pakai objek yang ditentukan aplikasi telah tercapai, ISpatialAudioObjectForHrtf::SetEndOfStream dipanggil untuk memberi tahu alur audio bahwa tidak ada lagi audio yang akan ditulis menggunakan objek ini dan objek diatur ke nullptr untuk membebaskan sumber dayanya.

Setelah menulis data ke semua objek audio Anda, panggil ISpatialAudioRenderStreamForHrtf::EndUpdatingAudioObjects untuk memberi tahu sistem bahwa data siap untuk dirender. Anda hanya dapat memanggil GetBuffer dalam panggilan antara BeginUpdatingAudioObjects dan EndUpdatingAudioObjects.

// 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);

Setelah selesai merender audio spasial, hentikan aliran audio spasial dengan memanggil ISpatialAudioRenderStreamForHrtf::Stop. Jika Anda tidak akan menggunakan aliran lagi, bebaskan sumber dayanya dengan memanggil 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);

Contoh kode berikut menunjukkan implementasi metode pembantu CalculateEmitterConeOrientationMatrix yang digunakan dalam contoh di atas untuk menghitung matriks orientasi mengingat arah objek 3D menunjuk.

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();
}

Suara Spasial

ISpatialAudioClient

ISpatialAudioObject