Loupe

[C++] Utilisation des APIs XAudio2

L’exemple de code associé à cet article est disponible ici :


Attention cependant, les fichiers audio fournis dans le projet ne sont pas libres de droits et ne peuvent donc pas être réutilisés dans vos projet.

XAudio2 est une API bas niveau de lecture de son optimisée pour les scénarios à très faible latence, comme les jeux-vidéo ou les applications de création musicale. Elle est disponible sous toutes les plateformes Microsoft : Windows, Windows Phone, Xbox 360 et bientôt Xbox One, et fournit un nombre de fonctionnalités assez impressionnant : Mixing (en mono, stéréo, 5.1, 7.1…), spatialisation 2D ou 3D, réverbs, pitching, filtres low-cut/high-cut, création d’effets customs, etc. pour des performances impressionnantes. En dehors du jeu vidéo, voici un exemple d’utilisation créative d’XAudio2 :

Le Live de C2C au Lancement de Windows 8

XAudio2 étant une api relativement récente (2007), ses designers l’ont conçues pour une utilisation exclusivement C++, et elle bénéficie donc d’un modèle objet très agréable à consommer, notamment dans des applications WinRT. Cet article a pour but d’introduire cette api avec 2 exemples sous Windows 8.1. Dans un premier temps, nous verrons comment lire un wav simple, puis un son en 3 parties, dont celle du milieu qui boucle en fonction d’une action utilisateur.

Démarrage d’XAudio2, chargement d’un wav, et lecture de son simple

Disclaimer :

Pour des raisons de lisibilité et de simplicité, le code fournit avec cet article ne fait pas tout ce qu’il devrait faire : les voix ne sont jamais détruites (ce qui est très mal car cela provoque un leak, et seul le premier HRESULT est vérifié). Ayez donc bien en tête cela : utilisez ce code comme une source de documentation, mais surtout pas en production.

XAudio2 est un moteur qui tourne en parallèle du code s’exécutant dans votre appli. On communique avec ce moteur en passant des ordres (lecture de buffer, activation d’effet etc.), et en recevant des évènements. Sur Windows, cela se traduit par un certain nombre de threads contrôlés par XAudio2 s’occupant du mixage du son, mais sur d’autres plateformes (Xbox One par exemple) cela peut aussi se traduire par une exécution de ce mixage sur du matériel dédié. Pour démarrer le moteur, on appellera la fonction XAudio2Create. Cette fonction produira un objet COM de type IXaudio2 qui nous permet d’interagir avec le moteur. On créera alors la « MasteringVoice » qui enverra le son qu’elle génère directement aux enceintes, puis on démarre le tout :

#pragma once
// this library requires to link to xaudio2.lib
#include <xaudio2.h>
namespace XAudio2Lib{
    public ref class SoundSystem sealed
    {
    private:
        Microsoft::WRL::ComPtr<IXAudio2> _engine;
        IXAudio2MasteringVoice* _masteringVoice;
    public:
        SoundSystem(){
            HRESULT hr = XAudio2Create(&_engine);
            if (hr != S_OK){
                throw ref new Platform::COMException(hr);
            }
            _engine->CreateMasteringVoice(&_masteringVoice);
            _engine->StartEngine();
        }
        virtual ~SoundSystem(){
            _masteringVoice->DestroyVoice();
        }
    };
}

Une fois que l’on a fait ca, il faut maintenant charger un wav. XAudio2 étant une librairie bas niveau, elle travaille directement avec des samples décodés (dans le format PCM, celui-là même qui est traité par les cartes sons, ou ADPCM pour un format compressé). Les fichiers WAV sont de simples conteneurs de données PCM avec un en-tête (précisant le nombre de canaux et leur taux d’échantillonnage) et potentiellement plusieurs zones de contenu. Cet article n’a pas pour but de rentrer dans les détails de la lecture de fichiers wav, aussi je vous redirigerai vers la lecture de cet article : http://msdn.microsoft.com/en-us/library/windows/desktop/ee415781(v=vs.85).aspx, ou au code source fournit avec mon article. Toujours est-il qu’on obtient :

- Une structure de type « WAVEFORMATEX » (équivalent à l’en-tête de notre wav)

- Un bloc de donnée (byte*) contenant les données de son à proprement parler.

Grâce à ces infos, on va pouvoir créer une nouvelle voie de type « IXAudio2SourceVoice » dont on branchera la sortie à notre Mastering Voice, et nous lui soumettrons les données PCM :

void PlaySound(SoundData^ soundData){
            IXAudio2SourceVoice* voice;

            // definition de la sortie audio:
            XAUDIO2_SEND_DESCRIPTOR sendToMaster = { 0 };
            sendToMaster.pOutputVoice = _masteringVoice;
            XAUDIO2_VOICE_SENDS sends = { 0 };
            sends.SendCount = 1;
            sends.pSends = &sendToMaster;

            // création de la voix
            _engine->CreateSourceVoice(&voice, soundData->getWaveFormat(), 0, 2.0f, nullptr, &sends);

            // création du buffer xaudio
            XAUDIO2_BUFFER buffer = { 0 };
            buffer.AudioBytes = soundData->getPCMSize();  //taille du buffer
            buffer.pAudioData = soundData->getPCMData();  //contenu du buffer
            buffer.Flags = XAUDIO2_END_OF_STREAM; //indique qu'il n'y aura pas d'autres buffers à jouer dans cette voix

            // lecture du buffer
            voice->SubmitSourceBuffer(&buffer);
            voice->Start();
        
        }

Comme vous pouvez le voir, on travaille à assez bas niveau (buffer contenant du contenu PCM, création manuelle des voix etc.), mais le contrôle obtenu est assez intéressant (création de graphe de voix, paramétrage d’effet par voix, possibilité d’utiliser des buffers plusieurs foix etc.).

Voyons donc comment aller plus loin avec un son composite !

Lecture d’un son composite

Nous allons maintenant voir comment créer un son composé de 3 partie : 1 intro qui sera jouée une fois, 1 zone de bouclage qui sera répétée n fois, et une fin (son qui nécessitera donc 3 fichiers wav). Dans l’appli de démo, le « Pointer Down » sur le bouton déclenchera le son, et le « Pointer Up » arrêtera la boucle.

Pour cette démo, j’aurai aussi besoin d’une nouvelle classe WinRT pour encapsuler ce son à 3 phases que j’appellerai ThreePartSound. En voici sa définition :

public ref class ThreePartSound sealed{
private:
    IXAudio2* _engine;
    IXAudio2MasteringVoice* _masterVoice;
    IXAudio2SourceVoice* _voice;
    SoundData^ _intro;
    SoundData^ _loopPart;
    SoundData^ _outro;
internal:
    ThreePartSound(IXAudio2* engine, IXAudio2MasteringVoice* masteringVoice,
        SoundData^ intro, SoundData^ loopPart, SoundData^ outro){
        _intro = intro;
        _loopPart = loopPart;
        _outro = outro;
        _engine = engine;
        _masterVoice = masteringVoice;

        XAUDIO2_SEND_DESCRIPTOR sendToMaster = { 0 };
        sendToMaster.pOutputVoice = _masterVoice;
        XAUDIO2_VOICE_SENDS sends = { 0 };
        sends.SendCount = 1;
        sends.pSends = &sendToMaster;

        // création de la voix
        _engine->CreateSourceVoice(&_voice, _intro->getWaveFormat(), 0, 2.0f, nullptr, &sends);


        XAUDIO2_BUFFER introBuffer = { 0 };
        introBuffer.AudioBytes = _intro->getPCMSize();  
        introBuffer.pAudioData = _intro->getPCMData();  


        XAUDIO2_BUFFER loopBuffer = { 0 };
        loopBuffer.AudioBytes = _loopPart->getPCMSize();
        loopBuffer.pAudioData = _loopPart->getPCMData();
        loopBuffer.LoopBegin = 0;
        int byteLength = loopBuffer.AudioBytes;
        // la longueur de la zone de bouclage est exprimée en nombre de sample. On veut qu'elle fasse
        // l'intégralité du buffer de son. On prend donc la taille en bytes, et on la divise par la taille d'un sample
        // (déduite du format du wave), et on passe l'info au buffer
        int sampleLength = byteLength / (_loopPart->getWaveFormat()->wBitsPerSample * _loopPart->getWaveFormat()->nChannels / 8);
        loopBuffer.LoopLength = sampleLength;
        loopBuffer.LoopCount = XAUDIO2_LOOP_INFINITE;


        // on joue le tout
        _voice->SubmitSourceBuffer(&introBuffer);
        _voice->SubmitSourceBuffer(&loopBuffer);
        _voice->Start();
    }
public:
    void ExitLoopZone(){
        
        // pour sortir d'une zone de bouclage, c'est très simple :
        // _voice->ExitLoop();
        // mais il faut gérer le cas où on n'est pas encore dans la zone de bouclage !
        // donc on flush les buffers qui n'ont pas encore commencé à être lu
        _voice->FlushSourceBuffers();
        // puis on soumet l'outro
        XAUDIO2_BUFFER outroBuffer = { 0 };
        outroBuffer.AudioBytes = _outro->getPCMSize();
        outroBuffer.pAudioData = _outro->getPCMData();
        _voice->SubmitSourceBuffer(&outroBuffer);
        // et on sort de la boucle (si on était déjà en train de lire le buffer de boucle)
        _voice->ExitLoop();
    }
};

Dans cet exemple de code, on voit bien 3 buffers distincts, dont un où l’on va spécifier une zone de bouclage. Cela nous permet de définir de manière interactive le moment où l’on sort de la boucle.

Petite astuce C++ / CX aussi dans ce code, vous pouvez voir que le constructeur est internal : cela assure le compilateur qu’il ne sera accessible qu’au sein du code C++, et donc les paramètres purement natifs (IXaudio2* et IXaudio2MasteringVoice*) sont autorisés ! L’objet sera donc créé à partir d’une méthode de la classe SoundSystem précédemment décrite :

ThreePartSound^ PlayThreePartSound(SoundData^ intro, SoundData^ loopPart, SoundData^ outro){
    return ref new ThreePartSound(_engine.Get(), _masteringVoice, intro, loopPart, outro);
} 

Ceci était donc une courte intro avec du simple playback de son. Il y’a pleins d’autres aspects d’XAudio2 passionnant à découvrir (spatialisation, reverb, filtres, XAPO, …), et l’API est vraiment très pratique à utiliser. Je vous invite à découvrir la doc très fournie sur la msdn (et valable pour toutes les plateformes) pour découvrir ces différents scénarios : http://msdn.microsoft.com/en-us/library/windows/desktop/hh405049(v=vs.85).aspx

Ces billets pourraient aussi vous intéresser

Vous nous direz ?!

Commentaires

comments powered by Disqus