Share via


Afficher le son spatial à l’aide d’objets audio spatiaux

Cet article présente quelques exemples simples qui illustrent comment implémenter le son spatial à l’aide d’objets audio spatiaux statiques, d’objets audio spatiaux dynamiques et d’objets audio spatiaux qui utilisent la fonction HRTF (Head Relative Transfer Function) de Microsoft. Les étapes d’implémentation de ces trois techniques sont très similaires et cet article fournit un exemple de code structuré similaire pour chaque technique. Pour obtenir des exemples complets d’implémentations audio spatiales réelles de bout en bout, consultez Référentiel github d’exemples Microsoft Spatial Sound. Pour obtenir une vue d’ensemble de Windows Sonic, la solution au niveau de la plateforme de Microsoft pour la prise en charge du son spatial sur Xbox et Windows, consultez Spatial Sound.

Rendu audio à l’aide d’objets audio spatiaux statiques

Un objet audio statique est utilisé pour restituer le son à l’un des 18 canaux audio statiques définis dans l’énumération AudioObjectType . Chacun de ces canaux représente un haut-parleur réel ou virtualisé à un point fixe dans l’espace qui ne se déplace pas au fil du temps. Les canaux statiques disponibles sur un appareil particulier dépendent du format sonore spatial utilisé. Pour obtenir la liste des formats pris en charge et leur nombre de canaux, consultez Spatial Sound.

Lorsque vous initialisez un flux audio spatial, vous devez spécifier les canaux statiques disponibles que le flux utilisera. Les exemples de définitions de constante suivants peuvent être utilisés pour spécifier des configurations d’orateur courantes et obtenir le nombre de canaux disponibles pour chacune d’elles.

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

La première étape du rendu audio spatial consiste à obtenir le point de terminaison audio auquel les données audio seront envoyées. Créez une instance de MMDeviceEnumerator et appelez GetDefaultAudioEndpoint pour obtenir le périphérique de rendu audio par défaut.

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

Lorsque vous créez un flux audio spatial, vous devez spécifier le format audio utilisé par le flux en fournissant une structure WAVEFORMATEX . Si vous lisez de l’audio à partir d’un fichier, le format est généralement déterminé par le format de fichier audio. Cet exemple utilise un format mono, 32 bits et 48 Hz.

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;

L’étape suivante dans le rendu audio spatial consiste à initialiser un flux audio spatial. Tout d’abord, obtenez un instance de ISpatialAudioClient en appelant IMMDevice::Activate. Appelez ISpatialAudioClient::IsAudioObjectFormatSupported pour vous assurer que le format audio que vous utilisez est pris en charge. Créez un événement que le pipeline audio utilisera pour informer l’application qu’elle est prête pour plus de données audio.

Remplissez une structure SpatialAudioObjectRenderStreamActivationParams qui sera utilisée pour initialiser le flux audio spatial. Dans cet exemple, le champ StaticObjectTypeMask est défini sur la constante ChannelMask_Stereo définie précédemment dans cet article, ce qui signifie que seuls les canaux avant droit et gauche peuvent être utilisés par le flux audio. Étant donné que cet exemple utilise uniquement des objets audio statiques et aucun objet dynamique, le champ MaxDynamicObjectCount est défini sur 0. Le champ Category est défini sur un membre de l’énumération AUDIO_STREAM_CATEGORY , qui définit la façon dont le système mélange le son de ce flux avec d’autres sources audio.

Appelez ISpatialAudioClient::ActivateSpatialAudioStream pour activer le flux.

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

Notes

Lorsque vous utilisez les interfaces ISpatialAudioClient sur un titre XDK (Xbox One Development Kit), vous devez d’abord appeler EnableSpatialAudio avant d’appeler IMMDeviceEnumerator::EnumAudioEndpoints ou IMMDeviceEnumerator::GetDefaultAudioEndpoint. Si vous ne le faites pas, une erreur E_NOINTERFACE est retournée à partir de l’appel à Activer. EnableSpatialAudio est disponible uniquement pour les titres XDK et n’a pas besoin d’être appelé pour plateforme Windows universelle applications s’exécutant sur Xbox One, ni pour les appareils non Xbox One.

 

Déclarez un pointeur pour un objet ISpatialAudioObject qui sera utilisé pour écrire des données audio dans un canal statique. Les applications classiques utilisent un objet pour chaque canal spécifié dans le champ StaticObjectTypeMask . Par souci de simplicité, cet exemple utilise uniquement un seul objet audio statique.

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

Avant d’entrer dans la boucle de rendu audio, appelez ISpatialAudioObjectRenderStream::Start pour indiquer au pipeline multimédia de commencer à demander des données audio. Cet exemple utilise un compteur pour arrêter le rendu audio après 5 secondes.

À l’intérieur de la boucle de rendu, attendez que l’événement d’achèvement de la mémoire tampon, fourni lors de l’initialisation du flux audio spatial, soit signalé. Vous devez définir une limite de délai d’attente raisonnable, par exemple 100 ms, lorsque vous attendez l’événement, car toute modification apportée au type de rendu ou au point de terminaison ne signale jamais cet événement. Dans ce cas, vous pouvez appeler ISpatialAudioObjectRenderStream::Reset pour tenter de réinitialiser le flux audio spatial.

Ensuite, appelez ISpatialAudioObjectRenderStream::BeginUpdatingAudioObjects pour informer le système que vous êtes sur le point de remplir les mémoires tampons des objets audio avec des données. Cette méthode retourne le nombre d’objets audio dynamiques disponibles, non utilisés dans cet exemple, et le nombre de trames de la mémoire tampon pour les objets audio rendus par ce flux.

Si aucun objet audio spatial statique n’a encore été créé, créez-en un ou plusieurs en appelant ISpatialAudioObjectRenderStream::ActivateSpatialAudioObject, en passant une valeur de l’énumération AudioObjectType indiquant le canal statique vers lequel l’audio de l’objet est rendu.

Ensuite, appelez ISpatialAudioObject::GetBuffer pour obtenir un pointeur vers la mémoire tampon audio de l’objet audio spatial. Cette méthode retourne également la taille de la mémoire tampon, en octets. Cet exemple utilise une méthode d’assistance, WriteToAudioObjectBuffer, pour remplir la mémoire tampon avec des données audio. Cette méthode est présentée plus loin dans cet article. Après avoir écrit dans la mémoire tampon, l’exemple vérifie si la durée de vie de 5 secondes de l’objet a été atteinte et, si c’est le cas, ISpatialAudioObject::SetEndOfStream est appelé pour informer le pipeline audio qu’aucun autre audio ne sera écrit à l’aide de cet objet et que l’objet est défini sur nullptr pour libérer ses ressources.

Après avoir écrit des données dans tous vos objets audio, appelez ISpatialAudioObjectRenderStream::EndUpdatingAudioObjects pour informer le système que les données sont prêtes pour le rendu. Vous pouvez uniquement appeler GetBuffer entre un appel à BeginUpdatingAudioObjects et 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();
};

Lorsque vous avez terminé le rendu audio spatial, arrêtez le flux audio spatial en appelant ISpatialAudioObjectRenderStream::Stop. Si vous n’envisagez pas d’utiliser à nouveau le flux, libérez ses ressources en appelant 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);

La méthode d’assistance WriteToAudioObjectBuffer écrit une mémoire tampon complète d’exemples ou le nombre d’échantillons restants spécifié par notre limite de temps définie par l’application. Cela peut également être déterminé, par exemple, par le nombre d’échantillons restants dans un fichier audio source. Une onde sin simple, dont la fréquence est mise à l’échelle par le paramètre d’entrée de fréquence , est générée et écrite dans la mémoire tampon.

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

Rendu audio à l’aide d’objets audio spatiaux dynamiques

Les objets dynamiques vous permettent de restituer l’audio à partir d’une position arbitraire dans l’espace, par rapport à l’utilisateur. La position et le volume d’un objet audio dynamique peuvent être modifiés au fil du temps. Les jeux utilisent généralement la position d’un objet 3D dans le monde du jeu pour spécifier la position de l’objet audio dynamique qui lui est associé. L’exemple suivant utilise une structure simple, My3dObject, pour stocker le jeu minimal de données nécessaire pour représenter un objet. Ces données incluent un pointeur vers un objet ISpatialAudioObject, la position, la vitesse, le volume et la fréquence de tonalité de l’objet, ainsi qu’une valeur qui stocke le nombre total d’images pour lesquelles l’objet doit restituer le son.

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

Les étapes d’implémentation des objets audio dynamiques sont en grande partie identiques à celles des objets audio statiques décrites ci-dessus. Tout d’abord, obtenez un point de terminaison 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);

Ensuite, initialisez le flux audio spatial. Obtenez un instance de ISpatialAudioClient en appelant IMMDevice::Activate. Appelez ISpatialAudioClient::IsAudioObjectFormatSupported pour vous assurer que le format audio que vous utilisez est pris en charge. Créez un événement que le pipeline audio utilisera pour informer l’application qu’elle est prête pour plus de données audio.

Appelez ISpatialAudioClient::GetMaxDynamicObjectCount pour récupérer le nombre d’objets dynamiques pris en charge par le système. Si cet appel retourne 0, les objets audio spatiaux dynamiques ne sont pas pris en charge ou activés sur l’appareil actuel. Pour plus d’informations sur l’activation de l’audio spatial et pour plus d’informations sur le nombre d’objets audio dynamiques disponibles pour différents formats audio spatiaux, consultez Spatial Sound.

Lorsque vous remplissez la structure SpatialAudioObjectRenderStreamActivationParams , définissez le champ MaxDynamicObjectCount sur le nombre maximal d’objets dynamiques que votre application utilisera.

Appelez ISpatialAudioClient::ActivateSpatialAudioStream pour activer le flux.

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

Voici un code spécifique à l’application nécessaire pour prendre en charge cet exemple, qui génère dynamiquement des objets audio positionnés de manière aléatoire et les stocke dans un vecteur.

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

Avant d’entrer dans la boucle de rendu audio, appelez ISpatialAudioObjectRenderStream::Start pour indiquer au pipeline multimédia de commencer à demander des données audio.

À l’intérieur de la boucle de rendu, attendez que l’événement d’achèvement de la mémoire tampon que nous avons fourni lors de l’initialisation du flux audio spatial soit signalé. Vous devez définir une limite de délai d’attente raisonnable, par exemple 100 ms, lorsque vous attendez l’événement, car toute modification apportée au type de rendu ou au point de terminaison ne signale jamais cet événement. Dans ce cas, vous pouvez appeler ISpatialAudioObjectRenderStream::Reset pour tenter de réinitialiser le flux audio spatial.

Ensuite, appelez ISpatialAudioObjectRenderStream::BeginUpdatingAudioObjects pour informer le système que vous êtes sur le point de remplir les mémoires tampons des objets audio avec des données. Cette méthode retourne le nombre d’objets audio dynamiques disponibles et le nombre d’images de la mémoire tampon pour les objets audio rendus par ce flux.

Chaque fois que le compteur de génération atteint la valeur spécifiée, nous allons activer un nouvel objet audio dynamique en appelant ISpatialAudioObjectRenderStream::ActivateSpatialAudioObject en spécifiant AudioObjectType_Dynamic. Si tous les objets audio dynamiques disponibles ont déjà été alloués, cette méthode retourne SPLAUDCLNT_E_NO_MORE_OBJECTS. Dans ce cas, vous pouvez choisir de libérer un ou plusieurs objets audio précédemment activés en fonction de la hiérarchisation propre à votre application. Une fois l’objet audio dynamique créé, il est ajouté à une nouvelle structure My3dObject , avec des valeurs aléatoires de position, de vitesse, de volume et de fréquence, qui est ensuite ajoutée à la liste des objets actifs.

Ensuite, effectuez une itération sur tous les objets actifs, représentés dans cet exemple avec la structure My3dObject définie par l’application . Pour chaque objet audio, appelez ISpatialAudioObject::GetBuffer pour obtenir un pointeur vers la mémoire tampon audio de l’objet audio spatial. Cette méthode retourne également la taille de la mémoire tampon, en octets. La méthode d’assistance, WriteToAudioObjectBuffer, pour remplir la mémoire tampon avec des données audio. Après avoir écrit dans la mémoire tampon, l’exemple met à jour la position de l’objet audio dynamique en appelant ISpatialAudioObject::SetPosition. Le volume de l’objet audio peut également être modifié en appelant SetVolume. Si vous ne mettez pas à jour la position ou le volume de l’objet, il conservera la position et le volume de la dernière fois qu’il a été défini. Si la durée de vie définie par l’application de l’objet a été atteinte, ISpatialAudioObject::SetEndOfStream est appelé pour informer le pipeline audio qu’il n’y aura plus d’audio écrit à l’aide de cet objet et que l’objet est défini sur nullptr pour libérer ses ressources.

Après avoir écrit des données dans tous vos objets audio, appelez ISpatialAudioObjectRenderStream::EndUpdatingAudioObjects pour informer le système que les données sont prêtes pour le rendu. Vous pouvez uniquement appeler GetBuffer entre un appel à BeginUpdatingAudioObjects et 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);

Lorsque vous avez terminé le rendu audio spatial, arrêtez le flux audio spatial en appelant ISpatialAudioObjectRenderStream::Stop. Si vous n’envisagez pas d’utiliser à nouveau le flux, libérez ses ressources en appelant 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);

Rendu audio à l’aide d’objets audio spatiaux dynamiques pour HRTF

Un autre ensemble d’API, ISpatialAudioRenderStreamForHrtf et ISpatialAudioObjectForHrtf, activent l’audio spatial qui utilise la fonction de transfert head-relative (HRTF) de Microsoft pour atténuer les sons afin de simuler la position de l’émetteur dans l’espace, par rapport à l’utilisateur, qui peut être modifiée au fil du temps. En plus de la position, les objets audio HRTF vous permettent de spécifier une orientation dans l’espace, une directivité dans laquelle le son est émis, comme une forme cône ou cardioïde, et un modèle de désintégration à mesure que l’objet se rapproche et s’éloigne de l’écouteur virtuel. Notez que ces interfaces HRTF ne sont disponibles que lorsque l’utilisateur a sélectionné Windows Sonic pour casque comme moteur audio spatial pour l’appareil. Pour plus d’informations sur la configuration d’un appareil pour utiliser Windows Sonic pour casque, consultez Spatial Sound.

Les API ISpatialAudioRenderStreamForHrtf et ISpatialAudioObjectForHrtf permettent à une application d’utiliser explicitement le chemin d’accès de rendu Windows Sonic pour casque directement. Ces API ne prennent pas en charge les formats sonores spatiaux tels que Dolby Atmos for Home Theater ou Dolby Atmos for Headphones, ni le changement de format de sortie contrôlé par le consommateur via le panneau de configuration audio, ni la lecture sur les haut-parleurs. Ces interfaces sont destinées à être utilisées dans Windows Mixed Reality applications qui souhaitent utiliser des fonctionnalités spécifiques à Windows Sonic pour casque (telles que les présélections environnementales et le déploiement basé sur la distance spécifié par programmation, en dehors des pipelines de création de contenu classiques). La plupart des jeux et des scénarios de réalité virtuelle préfèrent utiliser ISpatialAudioClient à la place. Les étapes d’implémentation des deux ensembles d’API étant presque identiques, il est possible d’implémenter les deux technologies et de basculer au moment de l’exécution en fonction de la fonctionnalité disponible sur l’appareil actuel.

Les applications de réalité mixte utilisent généralement la position d’un objet 3D dans le monde virtuel pour spécifier la position de l’objet audio dynamique qui lui est associé. L’exemple suivant utilise une structure simple, My3dObjectForHrtf, pour stocker le jeu minimal de données nécessaire pour représenter un objet. Ces données incluent un pointeur vers un ISpatialAudioObjectForHrtf, la position, l’orientation, la vitesse et la fréquence de tonalité de l’objet, ainsi qu’une valeur qui stocke le nombre total d’images pour lesquelles l’objet doit restituer le son.

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

Les étapes d’implémentation des objets audio HRTF dynamiques sont en grande partie les mêmes que celles pour les objets audio dynamiques décrites dans la section précédente. Tout d’abord, obtenez un point de terminaison 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);

Ensuite, initialisez le flux audio spatial. Obtenez un instance de ISpatialAudioClient en appelant IMMDevice::Activate. Appelez ISpatialAudioClient::IsAudioObjectFormatSupported pour vous assurer que le format audio que vous utilisez est pris en charge. Créez un événement que le pipeline audio utilisera pour informer l’application qu’elle est prête pour plus de données audio.

Appelez ISpatialAudioClient::GetMaxDynamicObjectCount pour récupérer le nombre d’objets dynamiques pris en charge par le système. Si cet appel retourne 0, les objets audio spatiaux dynamiques ne sont pas pris en charge ou activés sur l’appareil actuel. Pour plus d’informations sur l’activation de l’audio spatial et pour plus d’informations sur le nombre d’objets audio dynamiques disponibles pour différents formats audio spatiaux, consultez Spatial Sound.

Lorsque vous remplissez la structure SpatialAudioHrtfActivationParams , définissez le champ MaxDynamicObjectCount sur le nombre maximal d’objets dynamiques que votre application utilisera. Les paramètres d’activation pour HRTF prennent en charge quelques paramètres supplémentaires, tels qu’un SpatialAudioHrtfDistanceDecay, un SpatialAudioHrtfDirectivityUnion, un SpatialAudioHrtfEnvironmentType et un SpatialAudioHrtfOrientation, qui spécifient les valeurs par défaut de ces paramètres pour les nouveaux objets créés à partir du flux. Ces paramètres sont facultatifs. Définissez les champs sur nullptr pour ne fournir aucune valeur par défaut.

Appelez ISpatialAudioClient::ActivateSpatialAudioStream pour activer le flux.

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

Voici un code spécifique à l’application nécessaire pour prendre en charge cet exemple, qui génère dynamiquement des objets audio positionnés de manière aléatoire et les stocke dans un vecteur.

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

Avant d’entrer dans la boucle de rendu audio, appelez ISpatialAudioObjectRenderStreamForHrtf::Start pour indiquer au pipeline multimédia de commencer à demander des données audio.

À l’intérieur de la boucle de rendu, attendez que l’événement d’achèvement de la mémoire tampon que nous avons fourni lors de l’initialisation du flux audio spatial soit signalé. Vous devez définir une limite de délai d’attente raisonnable, par exemple 100 ms, lorsque vous attendez l’événement, car toute modification apportée au type de rendu ou au point de terminaison ne signale jamais cet événement. Dans ce cas, vous pouvez appeler ISpatialAudioRenderStreamForHrtf::Reset pour tenter de réinitialiser le flux audio spatial.

Ensuite, appelez ISpatialAudioRenderStreamForHrtf::BeginUpdatingAudioObjects pour informer le système que vous êtes sur le point de remplir les mémoires tampons des objets audio avec des données. Cette méthode retourne le nombre d’objets audio dynamiques disponibles, non utilisés dans cet exemple, et le nombre de trames de la mémoire tampon pour les objets audio rendus par ce flux.

Chaque fois que le compteur de génération atteint la valeur spécifiée, nous allons activer un nouvel objet audio dynamique en appelant ISpatialAudioRenderStreamForHrtf::ActivateSpatialAudioObjectForHrtf en spécifiant AudioObjectType_Dynamic. Si tous les objets audio dynamiques disponibles ont déjà été alloués, cette méthode retourne SPLAUDCLNT_E_NO_MORE_OBJECTS. Dans ce cas, vous pouvez choisir de libérer un ou plusieurs objets audio précédemment activés en fonction de la hiérarchisation propre à votre application. Une fois l’objet audio dynamique créé, il est ajouté à une nouvelle structure My3dObjectForHrtf , avec des valeurs aléatoires de position, de rotation, de vélocité, de volume et de fréquence, qui est ensuite ajoutée à la liste des objets actifs.

Ensuite, effectuez une itération sur tous les objets actifs, représentés dans cet exemple avec la structure My3dObjectForHrtf définie par l’application. Pour chaque objet audio, appelez ISpatialAudioObjectForHrtf::GetBuffer pour obtenir un pointeur vers la mémoire tampon audio de l’objet audio spatial. Cette méthode retourne également la taille de la mémoire tampon, en octets. La méthode d’assistance WriteToAudioObjectBuffer, répertoriée précédemment dans cet article, pour remplir la mémoire tampon avec des données audio. Après avoir écrit dans la mémoire tampon, l’exemple met à jour la position et l’orientation de l’objet audio HRTF en appelant ISpatialAudioObjectForHrtf::SetPosition et ISpatialAudioObjectForHrtf::SetOrientation. Dans cet exemple, une méthode d’assistance, CalculateEmitterConeOrientationMatrix, est utilisée pour calculer la matrice d’orientation en fonction de la direction vers laquelle pointe l’objet 3D. L’implémentation de cette méthode est illustrée ci-dessous. Le volume de l’objet audio peut également être modifié en appelant ISpatialAudioObjectForHrtf::SetGain. Si vous ne mettez pas à jour la position, l’orientation ou le volume de l’objet, il conservera la position, l’orientation et le volume de la dernière fois qu’il a été défini. Si la durée de vie définie par l’application de l’objet a été atteinte, ISpatialAudioObjectForHrtf::SetEndOfStream est appelé pour informer le pipeline audio qu’il n’y aura plus d’audio écrit à l’aide de cet objet et que l’objet est défini sur nullptr pour libérer ses ressources.

Après avoir écrit des données dans tous vos objets audio, appelez ISpatialAudioRenderStreamForHrtf::EndUpdatingAudioObjects pour informer le système que les données sont prêtes pour le rendu. Vous pouvez uniquement appeler GetBuffer entre un appel à BeginUpdatingAudioObjects et 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);

Lorsque vous avez terminé le rendu audio spatial, arrêtez le flux audio spatial en appelant ISpatialAudioRenderStreamForHrtf::Stop. Si vous ne souhaitez pas utiliser à nouveau le flux, libérez ses ressources en appelant 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);

L’exemple de code suivant montre l’implémentation de la méthode d’assistance CalculateEmitterConeOrientationMatrix utilisée dans l’exemple ci-dessus pour calculer la matrice d’orientation en fonction de la direction vers laquelle pointe l’objet 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();
}

Son spatial

ISpatialAudioClient

ISpatialAudioObject