Guide pour travailler avec OpenAL en C ++. Partie 1: jouer du son

Votre jeu a besoin de son! Vous avez probablement déjà utilisé OpenGL pour dessiner à l'écran. Vous avez compris son API, et vous vous êtes donc tourné vers OpenAL parce que le nom semble familier.

Eh bien, la bonne nouvelle est qu'OpenAL possède également une API très familière. Il a été initialement conçu pour simuler l'API de spécification OpenGL. C'est pourquoi je l'ai choisi parmi les nombreux systèmes de sonorisation pour les jeux; en outre, il est multiplateforme.

Dans cet article, je parlerai en détail du code nécessaire pour utiliser OpenAL dans un jeu écrit en C ++. Nous discuterons des sons, de la musique et du positionnement sonore dans l'espace 3D avec des exemples de code.

Histoire d'OpenAL


J'essaierai d'être bref. Comme mentionné ci-dessus, il a été intentionnellement conçu comme une imitation de l'API OpenGL, et il y a une raison à cela. Il s'agit d'une API pratique qui est connue de beaucoup, et si les graphiques sont un côté du moteur de jeu, alors le son devrait être différent. Initialement, OpenAL était censé être open-source, mais quelque chose s'est passé ... Les

gens ne sont pas aussi intéressés par le son que par les graphiques, donc Creative a finalement fait d'OpenAL sa propriété, et l' implémentation de référence est désormais propriétaire et non gratuite. Mais! La spécification OpenAL est toujours une norme «ouverte», c'est-à-dire qu'elle est publiée .

De temps en temps, les spécifications sont modifiées, mais pas beaucoup. Le son ne change pas aussi vite que les graphismes, car il n'y a pas de besoin particulier pour cela.

La spécification ouverte a permis à d'autres personnes de créer une implémentation open source de la spécification. Une telle implémentation est OpenAL Soft , et franchement, cela n'a aucun sens d'en chercher d'autres. C'est l'implémentation que j'utiliserai, et je vous recommande de l'utiliser également.

Elle est multiplateforme. Il est implémenté assez curieusement - en fait, à l'intérieur de la bibliothèque utilise d'autres API sonores présentes dans votre système. Sous Windows, il utilise DirectSound , sous Unix, OSS . Grâce à cela, elle a pu devenir multiplateforme; en substance, c'est un grand nom pour l'API wrapper.

Vous pouvez vous inquiéter de la vitesse de cette API. Mais ne t'inquiète pas. C'est le même son et il ne crée pas une charge importante, il ne nécessite donc pas les grandes optimisations requises par l'API graphique.

Mais assez d'histoire, passons à la technologie.

De quoi avez-vous besoin pour écrire du code dans OpenAL?


Vous devez créer OpenAL Soft dans la chaîne d'outils de votre choix. Il s'agit d'un processus très simple que vous pouvez suivre conformément aux instructions de la section Installation source . Je n'ai jamais eu de problème avec cela, mais si vous avez des difficultés, écrivez un commentaire sous l'article d'origine ou écrivez à la liste de diffusion OpenAL Soft .

Ensuite, vous aurez besoin de quelques fichiers audio et d'un moyen de les télécharger. Le chargement de données audio dans des tampons et des détails subtils de divers formats audio n'entrent pas dans le cadre de cet article, mais vous pouvez en savoir plus sur le téléchargement et le streaming de fichiers Ogg / Vorbis . Le téléchargement de fichiers WAV est très simple, il existe déjà des centaines d'articles sur Internet à ce sujet.

La tâche de trouver des fichiers audio que vous devez décider vous-même. Il existe de nombreux bruits et explosions sur Internet que vous pouvez télécharger. Si vous avez une rumeur, vous pouvez essayer d' écrire votre propre musique chiptune [ traduction sur Habré].

Gardez également à portée de main le Guide du programmeur d'OpenALSoft . Cette documentation est bien meilleur pdf avec spécialisation "officielle".

En fait, c'est tout. Nous supposerons que vous savez déjà comment écrire du code, utilisez l'IDE et la chaîne d'outils.

Présentation de l'API OpenAL


Comme je l'ai dit plusieurs fois, c'est similaire à l'API OpenGL. La similitude réside dans le fait qu'elle est basée sur des états et que vous interagissez avec des descripteurs / identifiants, et non directement avec les objets eux-mêmes.

Il existe des différences entre les conventions d'API dans OpenGL et OpenAL, mais elles ne sont pas significatives. Dans OpenGL, vous devez effectuer des appels OS spéciaux pour générer un contexte de rendu. Ces défis sont différents pour différents systèmes d'exploitation et ne font pas vraiment partie de la spécification OpenGL. Dans OpenAL, tout est différent - les fonctions de création de contexte font partie de la spécification et sont les mêmes quel que soit le système d'exploitation.

Lorsque vous interagissez avec l'API, il existe trois principaux types d'objets avec lesquels vous interagissez. Les auditeurs(«Listeners») est l'emplacement des «oreilles» situées dans l'espace 3D (il n'y a toujours qu'un seul auditeur). Les sources («sources») sont des «haut-parleurs» qui émettent du son, là encore dans l'espace 3D. L'auditeur et les sources peuvent être déplacés dans l'espace et en fonction de cela, ce que vous entendez à travers les haut-parleurs dans le jeu change.

Les derniers objets sont des tampons . Ils stockent des échantillons de sons que les sources joueront pour les auditeurs.

Il existe également des modes que le jeu utilise pour changer la façon dont l'audio est traité via OpenAL.

Sources


Comme mentionné ci-dessus, ces objets sont des sources de sons. Ils peuvent être réglés en position et en direction, et ils sont associés à un tampon de lecture de données audio.

Auditeur


Le seul ensemble d '"oreilles" du jeu. Ce que l'auditeur entend est reproduit par les haut-parleurs de l'ordinateur. Il a également un poste.

Tampons


Dans OpenGL, leur équivalent est Texture2D. Il s'agit essentiellement des données audio que la source reproduit.

Types de données


Pour pouvoir prendre en charge le code multiplateforme, OpenAL effectue une certaine séquence d'actions et définit certains types de données. En fait, il suit OpenGL si précisément que nous pouvons même convertir directement les types OpenAL en types OpenGL. Le tableau ci-dessous les répertorie ainsi que leurs équivalents.

Type ouvertType openalcType openglC ++ TypedefLa description
ALbooleanALCbooleanGLbooleanstd::int8_tValeur booléenne 8 bits
ALbyteALCbyteGLbytestd::int8_tValeur entière sur 8 bits du code supplémentaire avec un signe
ALubyteALCubyteGLubytestd::uint8_tValeur entière non signée 8 bits
ALcharALCcharGLcharcharsymbole
ALshortALCshortGLshortstd::int16_tValeur entière signée 16 bits
ALushortALCushortGLushortstd::uint16_t16-
ALintALCintGLintstd::int32_t32-
ALuintALCuintGLuintstd::uint32_t32-
ALsizeiALCsizeiGLsizeistd::int32_t32-
ALenumALCenumGLenumstd::uint32_t32-
ALfloatALCfloatGLfloatfloat32- IEEE 754
ALdoubleALCdoubleGLdoubledouble64- IEEE 754
ALvoidALCvoidGLvoidvoid

OpenAL


Il y a un article sur la façon de simplifier la reconnaissance des erreurs OpenAL , mais par souci d'exhaustivité, je vais le répéter ici. Il existe deux types d'appels OpenAL API: réguliers et contextuels.

Les appels de contexte commençant par alcsont similaires aux appels OpenGL win32 pour obtenir le contexte de rendu ou leurs homologues sous Linux. Le son est une chose assez simple pour que tous les systèmes d'exploitation aient les mêmes appels. Les appels ordinaires commencent par al. Pour obtenir des erreurs dans les appels contextuels, nous appelons alcGetError; dans le cas d'appels réguliers, nous appelons alGetError. Ils renvoient une valeur ALCenumou une valeur ALenumqui répertorie simplement les erreurs possibles.

Nous allons maintenant considérer un seul cas, mais dans tout le reste, ils sont presque les mêmes. Relevons les défis habituels al. Tout d'abord, créez une macro de préprocesseur pour faire le travail ennuyeux de transmission des détails:

#define alCall(function, ...) alCallImpl(__FILE__, __LINE__, function, __VA_ARGS__)

Théoriquement, votre compilateur peut ne pas prendre __FILE__en charge non plus __LINE__, mais, pour être honnête, je serais surpris si cela se révélait être le cas. __VA_ARGS__indique un nombre variable d'arguments pouvant être transmis à cette macro.

Ensuite, nous implémentons une fonction qui reçoit manuellement la dernière erreur signalée et affiche une valeur claire dans le flux d'erreurs standard.

bool check_al_errors(const std::string& filename, const std::uint_fast32_t line)
{
    ALenum error = alGetError();
    if(error != AL_NO_ERROR)
    {
        std::cerr << "***ERROR*** (" << filename << ": " << line << ")\n" ;
        switch(error)
        {
        case AL_INVALID_NAME:
            std::cerr << "AL_INVALID_NAME: a bad name (ID) was passed to an OpenAL function";
            break;
        case AL_INVALID_ENUM:
            std::cerr << "AL_INVALID_ENUM: an invalid enum value was passed to an OpenAL function";
            break;
        case AL_INVALID_VALUE:
            std::cerr << "AL_INVALID_VALUE: an invalid value was passed to an OpenAL function";
            break;
        case AL_INVALID_OPERATION:
            std::cerr << "AL_INVALID_OPERATION: the requested operation is not valid";
            break;
        case AL_OUT_OF_MEMORY:
            std::cerr << "AL_OUT_OF_MEMORY: the requested operation resulted in OpenAL running out of memory";
            break;
        default:
            std::cerr << "UNKNOWN AL ERROR: " << error;
        }
        std::cerr << std::endl;
        return false;
    }
    return true;
}

Il n'y a pas beaucoup d'erreurs possibles. Les explications que j'ai écrites dans le code sont les seules informations que vous recevrez sur ces erreurs, mais la spécification explique pourquoi une fonction particulière peut renvoyer une erreur spécifique.

Ensuite, nous implémentons deux fonctions de modèle différentes qui encapsuleront tous nos appels OpenGL.

template<typename alFunction, typename... Params>
auto alCallImpl(const char* filename, 
                const std::uint_fast32_t line, 
                alFunction function, 
                Params... params)
->typename std::enable_if_t<!std::is_same_v<void,decltype(function(params...))>,decltype(function(params...))>
{
    auto ret = function(std::forward<Params>(params)...);
    check_al_errors(filename,line);
    return ret;
}

template<typename alcFunction, typename... Params>
auto alcCallImpl(const char* filename, 
                 const std::uint_fast32_t line, 
                 alcFunction function, 
                 ALCdevice* device, 
                 Params... params)
->typename std::enable_if_t<std::is_same_v<void,decltype(function(params...))>,bool>
{
    function(std::forward<Params>(params)...);
    return check_alc_errors(filename,line,device);
}

Il y en a deux, car le premier est utilisé pour les fonctions OpenAL qui retournent voidet le second est utilisé lorsque la fonction renvoie une valeur non vide. Si vous n'êtes pas très familier avec les modèles de métaprogrammation en C ++, jetez un œil aux parties du code c std::enable_if. Ils déterminent lesquelles de ces fonctions de modèle sont implémentées par le compilateur pour chaque appel de fonction.

Et maintenant la même chose pour les appels alc:

#define alcCall(function, device, ...) alcCallImpl(__FILE__, __LINE__, function, device, __VA_ARGS__)

bool check_alc_errors(const std::string& filename, const std::uint_fast32_t line, ALCdevice* device)
{
    ALCenum error = alcGetError(device);
    if(error != ALC_NO_ERROR)
    {
        std::cerr << "***ERROR*** (" << filename << ": " << line << ")\n" ;
        switch(error)
        {
        case ALC_INVALID_VALUE:
            std::cerr << "ALC_INVALID_VALUE: an invalid value was passed to an OpenAL function";
            break;
        case ALC_INVALID_DEVICE:
            std::cerr << "ALC_INVALID_DEVICE: a bad device was passed to an OpenAL function";
            break;
        case ALC_INVALID_CONTEXT:
            std::cerr << "ALC_INVALID_CONTEXT: a bad context was passed to an OpenAL function";
            break;
        case ALC_INVALID_ENUM:
            std::cerr << "ALC_INVALID_ENUM: an unknown enum value was passed to an OpenAL function";
            break;
        case ALC_OUT_OF_MEMORY:
            std::cerr << "ALC_OUT_OF_MEMORY: an unknown enum value was passed to an OpenAL function";
            break;
        default:
            std::cerr << "UNKNOWN ALC ERROR: " << error;
        }
        std::cerr << std::endl;
        return false;
    }
    return true;
}

template<typename alcFunction, typename... Params>
auto alcCallImpl(const char* filename, 
                 const std::uint_fast32_t line, 
                 alcFunction function, 
                 ALCdevice* device, 
                 Params... params)
->typename std::enable_if_t<std::is_same_v<void,decltype(function(params...))>,bool>
{
    function(std::forward<Params>(params)...);
    return check_alc_errors(filename,line,device);
}

template<typename alcFunction, typename ReturnType, typename... Params>
auto alcCallImpl(const char* filename,
                 const std::uint_fast32_t line,
                 alcFunction function,
                 ReturnType& returnValue,
                 ALCdevice* device, 
                 Params... params)
->typename std::enable_if_t<!std::is_same_v<void,decltype(function(params...))>,bool>
{
    returnValue = function(std::forward<Params>(params)...);
    return check_alc_errors(filename,line,device);
}

Le plus grand changement est l'inclusion device, que tous les appels utilisent alc, ainsi que l'utilisation correspondante des erreurs de style ALCenumet ALC_. Ils ont l'air très similaires, et pendant très longtemps de petits changements de alà alcgravement endommagé mon code et ma compréhension, j'ai donc continué à lire juste en haut c.

C'est tout. En règle générale, un appel OpenAL en C ++ ressemble à l'une des options suivantes:

/* example #1 */
alGenSources(1, &source);
ALenum error = alGetError();
if(error != AL_NO_ERROR)
{
    /* handle different possibilities */
}

/* example #2 */
alcCaptureStart(&device);
ALCenum error = alcGetError();
if(error != ALC_NO_ERROR)
{
    /* handle different possibilities */
}

/* example #3 */
const ALchar* sz = alGetString(param);
ALenum error = alGetError();
if(error != AL_NO_ERROR)
{
    /* handle different possibilities */
}

/* example #4 */
const ALCchar* sz = alcGetString(&device, param);
ALCenum error = alcGetError();
if(error != ALC_NO_ERROR)
{
    /* handle different possibilities */
}

Mais maintenant, nous pouvons le faire comme ceci:

/* example #1 */
if(!alCall(alGenSources, 1, &source))
{
    /* error occurred */
}

/* example #2 */
if(!alcCall(alcCaptureStart, &device))
{
    /* error occurred */
}

/* example #3 */
const ALchar* sz;
if(!alCall(alGetString, sz, param))
{
    /* error occurred */
}

/* example #4 */
const ALCchar* sz;
if(!alcCall(alcGetString, sz, &device, param))
{
    /* error occurred */
}

Cela peut vous sembler étrange, mais c'est plus pratique pour moi. Bien sûr, vous pouvez choisir une structure différente.

Téléchargez les fichiers .wav


Vous pouvez soit les télécharger vous-même, soit utiliser la bibliothèque. Voici une implémentation open-source de chargement de fichiers .wav . Je suis fou, donc je le fais moi-même:

std::int32_t convert_to_int(char* buffer, std::size_t len)
{
    std::int32_t a = 0;
    if(std::endian::native == std::endian::little)
        std::memcpy(&a, buffer, len);
    else
        for(std::size_t i = 0; i < len; ++i)
            reinterpret_cast<char*>(&a)[3 - i] = buffer[i];
    return a;
}

bool load_wav_file_header(std::ifstream& file,
                          std::uint8_t& channels,
                          std::int32_t& sampleRate,
                          std::uint8_t& bitsPerSample,
                          ALsizei& size)
{
    char buffer[4];
    if(!file.is_open())
        return false;

    // the RIFF
    if(!file.read(buffer, 4))
    {
        std::cerr << "ERROR: could not read RIFF" << std::endl;
        return false;
    }
    if(std::strncmp(buffer, "RIFF", 4) != 0)
    {
        std::cerr << "ERROR: file is not a valid WAVE file (header doesn't begin with RIFF)" << std::endl;
        return false;
    }

    // the size of the file
    if(!file.read(buffer, 4))
    {
        std::cerr << "ERROR: could not read size of file" << std::endl;
        return false;
    }

    // the WAVE
    if(!file.read(buffer, 4))
    {
        std::cerr << "ERROR: could not read WAVE" << std::endl;
        return false;
    }
    if(std::strncmp(buffer, "WAVE", 4) != 0)
    {
        std::cerr << "ERROR: file is not a valid WAVE file (header doesn't contain WAVE)" << std::endl;
        return false;
    }

    // "fmt/0"
    if(!file.read(buffer, 4))
    {
        std::cerr << "ERROR: could not read fmt/0" << std::endl;
        return false;
    }

    // this is always 16, the size of the fmt data chunk
    if(!file.read(buffer, 4))
    {
        std::cerr << "ERROR: could not read the 16" << std::endl;
        return false;
    }

    // PCM should be 1?
    if(!file.read(buffer, 2))
    {
        std::cerr << "ERROR: could not read PCM" << std::endl;
        return false;
    }

    // the number of channels
    if(!file.read(buffer, 2))
    {
        std::cerr << "ERROR: could not read number of channels" << std::endl;
        return false;
    }
    channels = convert_to_int(buffer, 2);

    // sample rate
    if(!file.read(buffer, 4))
    {
        std::cerr << "ERROR: could not read sample rate" << std::endl;
        return false;
    }
    sampleRate = convert_to_int(buffer, 4);

    // (sampleRate * bitsPerSample * channels) / 8
    if(!file.read(buffer, 4))
    {
        std::cerr << "ERROR: could not read (sampleRate * bitsPerSample * channels) / 8" << std::endl;
        return false;
    }

    // ?? dafaq
    if(!file.read(buffer, 2))
    {
        std::cerr << "ERROR: could not read dafaq" << std::endl;
        return false;
    }

    // bitsPerSample
    if(!file.read(buffer, 2))
    {
        std::cerr << "ERROR: could not read bits per sample" << std::endl;
        return false;
    }
    bitsPerSample = convert_to_int(buffer, 2);

    // data chunk header "data"
    if(!file.read(buffer, 4))
    {
        std::cerr << "ERROR: could not read data chunk header" << std::endl;
        return false;
    }
    if(std::strncmp(buffer, "data", 4) != 0)
    {
        std::cerr << "ERROR: file is not a valid WAVE file (doesn't have 'data' tag)" << std::endl;
        return false;
    }

    // size of data
    if(!file.read(buffer, 4))
    {
        std::cerr << "ERROR: could not read data size" << std::endl;
        return false;
    }
    size = convert_to_int(buffer, 4);

    /* cannot be at the end of file */
    if(file.eof())
    {
        std::cerr << "ERROR: reached EOF on the file" << std::endl;
        return false;
    }
    if(file.fail())
    {
        std::cerr << "ERROR: fail state set on the file" << std::endl;
        return false;
    }

    return true;
}

char* load_wav(const std::string& filename,
               std::uint8_t& channels,
               std::int32_t& sampleRate,
               std::uint8_t& bitsPerSample,
               ALsizei& size)
{
    std::ifstream in(filename, std::ios::binary);
    if(!in.is_open())
    {
        std::cerr << "ERROR: Could not open \"" << filename << "\"" << std::endl;
        return nullptr;
    }
    if(!load_wav_file_header(in, channels, sampleRate, bitsPerSample, size))
    {
        std::cerr << "ERROR: Could not load wav header of \"" << filename << "\"" << std::endl;
        return nullptr;
    }

    char* data = new char[size];

    in.read(data, size);

    return data;
}

Je ne vais pas expliquer le code, car ce n'est pas entièrement dans le sujet de notre article; mais c'est très évident si vous le lisez en parallèle avec la spécification du fichier WAV .

Initialisation et destruction


Nous devons d'abord initialiser OpenAL, puis, comme tout bon programmeur, le terminer lorsque nous avons fini de travailler avec. Il est utilisé lors de l'initialisation ALCdevice(notez que ce ALCn'est pas le cas AL ), qui représente essentiellement quelque chose sur votre ordinateur pour jouer de la musique de fond et l'utilise ALCcontext.

ALCdevicesimilaire au choix d'une carte graphique. sur lequel votre jeu OpenGL sera rendu. ALCcontextsimilaire au contexte de rendu que vous souhaitez créer (unique au système d'exploitation) pour OpenGL.

Alcdevice


Un périphérique OpenAL est ce par quoi le son est émis, que ce soit une carte son ou une puce, mais théoriquement, cela peut être beaucoup de choses différentes. Semblable à la façon dont la sortie standard iostreampeut être une imprimante plutôt qu'un écran, un périphérique peut être un fichier ou même un flux de données.

Cependant, pour la programmation de jeux, ce sera un périphérique audio, et généralement nous voulons que ce soit un périphérique de sortie audio standard dans le système.

Pour obtenir une liste des appareils disponibles dans le système, vous pouvez les demander avec cette fonction:

bool get_available_devices(std::vector<std::string>& devicesVec, ALCdevice* device)
{
    const ALCchar* devices;
    if(!alcCall(alcGetString, devices, device, nullptr, ALC_DEVICE_SPECIFIER))
        return false;

    const char* ptr = devices;

    devicesVec.clear();

    do
    {
        devicesVec.push_back(std::string(ptr));
        ptr += devicesVec.back().size() + 1;
    }
    while(*(ptr + 1) != '\0');

    return true;
}

C'est en fait juste un wrapper autour d'un wrapper autour d'un appel alcGetString. La valeur de retour est un pointeur vers une liste de chaînes séparées par une valeur nullet se terminant par deux valeurs null. Ici, le wrapper le transforme simplement en un vecteur qui nous convient.

Heureusement, nous n'avons pas besoin de faire ça! Dans le cas général, comme je le soupçonne, la plupart des jeux peuvent simplement émettre du son vers l'appareil par défaut, quel qu'il soit. Je vois rarement les options pour changer le périphérique audio à travers lequel vous souhaitez émettre du son. Par conséquent, pour initialiser le périphérique OpenAL, nous utilisons un appel alcOpenDevice. Cet appel est légèrement différent de tout le reste, car il ne spécifie pas l'état d'erreur qui peut être obtenu via alcGetError, nous l'appelons donc comme une fonction normale:

ALCdevice* openALDevice = alcOpenDevice(nullptr);
if(!openALDevice)
{
    /* fail */
}

Si vous avez répertorié les périphériques comme indiqué ci-dessus et que vous souhaitez que l'utilisateur en sélectionne un, vous devez transférer son nom à la alcOpenDeviceplace nullptr. Envoi de nullptrcommandes pour ouvrir l'appareil par défaut . La valeur de retour est soit le périphérique correspondant, soit en nullptrcas d'erreur.

Selon que vous avez terminé l'énumération ou non, une erreur peut arrêter le programme sur les pistes. Aucun périphérique = Aucun OpenAL; pas OpenAL = pas de son; pas de son = pas de jeu.

La dernière chose que nous faisons lors de la fermeture d'un programme est de le terminer correctement.

ALCboolean closed;
if(!alcCall(alcCloseDevice, closed, openALDevice, openALDevice))
{
    /* do we care? */
}

À ce stade, si l'achèvement n'était pas possible, cela n'est plus important pour nous. Avant de fermer l'appareil, nous devons fermer tous les contextes créés, cependant, selon mon expérience, cet appel complète également le contexte. Mais nous le ferons bien. Si vous terminez tout avant de passer un appel alcCloseDevice, il ne devrait pas y avoir d'erreurs et si, pour une raison quelconque, elles se sont produites, vous ne pouvez rien y faire.

Vous avez peut-être remarqué que les appels provenant de l' alcCallenvoi de deux copies de l'appareil. Cela s'est produit en raison du fonctionnement de la fonction de modèle - un est nécessaire pour la vérification des erreurs et le second est utilisé comme paramètre de fonction.

Théoriquement, je peux améliorer la fonction de modèle afin qu'elle passe le premier paramètre pour la vérification des erreurs et l'envoie toujours à la fonction; mais je suis paresseux pour le faire. Je vais laisser cela comme vos devoirs.

Notre ALCcontext


La deuxième partie de l'initialisation est le contexte. Comme précédemment, il est similaire au contexte de rendu d'OpenGL. Il peut y avoir plusieurs contextes dans un programme et nous pouvons basculer entre eux, mais nous n'en aurons pas besoin. Chaque contexte a son propre écouteur et ses propres sources , et ils ne peuvent pas être transmis entre les contextes.

C'est peut-être utile dans les logiciels de traitement du son. Cependant, pour les jeux dans 99,9% des cas, un seul contexte suffit.

La création d'un nouveau contexte est très simple:

ALCcontext* openALContext;
if(!alcCall(alcCreateContext, openALContext, openALDevice, openALDevice, nullptr) || !openALContext)
{
    std::cerr << "ERROR: Could not create audio context" << std::endl;
    /* probably exit program */
}

Nous devons communiquer pour ce que ALCdevicenous voulons créer un contexte; nous pouvons également transmettre une liste facultative de clés zéro et de valeurs ALCint, qui sont des attributs avec lesquels le contexte doit être créé.

Honnêtement, je ne sais même pas dans quelle situation le passage d'attribut est utile. Votre jeu fonctionnera sur un ordinateur ordinaire avec les fonctionnalités sonores habituelles. Les attributs ont des valeurs par défaut, selon l'ordinateur, ce n'est donc pas particulièrement important. Mais au cas où vous en auriez encore besoin:

Nom d'attributLa description
ALC_FREQUENCYFréquence de mixage vers le tampon de sortie, mesurée en Hz
ALC_REFRESHIntervalles de mise à jour, mesurés en Hz
ALC_SYNC0ou 1indiquer s'il doit s'agir d'un contexte synchrone ou asynchrone
ALC_MONO_SOURCESUne valeur qui vous indique le nombre de sources que vous utiliserez et qui nécessitent la capacité de traiter des données audio monophoniques. Il ne limite pas le montant maximum, il vous permet simplement d'être plus efficace lorsque vous le savez à l'avance.
ALC_STEREO_SOURCESLa même chose, mais pour les données stéréo.

Si vous obtenez des erreurs, cela est probablement dû au fait que les attributs souhaités sont impossibles ou que vous ne pouvez pas créer un autre contexte pour le périphérique pris en charge; cela entraînera une erreur ALC_INVALID_VALUE. Si vous passez devant un appareil invalide, vous obtiendrez une erreur ALC_INVALID_DEVICE, mais, bien sûr, nous vérifions déjà cette erreur.

Créer un contexte ne suffit pas. Nous devons encore l'actualiser - il ressemble à un contexte de rendu Windows OpenGL, non? C'est le même.

ALCboolean contextMadeCurrent = false;
if(!alcCall(alcMakeContextCurrent, contextMadeCurrent, openALDevice, openALContext)
   || contextMadeCurrent != ALC_TRUE)
{
    std::cerr << "ERROR: Could not make audio context current" << std::endl;
    /* probably exit or give up on having sound */
}

Il est nécessaire d'actualiser le contexte pour toute opération ultérieure avec le contexte (ou avec les sources et les écouteurs qu'il contient). L'opération retournera trueou false, la seule valeur d'erreur possible transmise alcGetErrorest celle ALC_INVALID_CONTEXTqui est claire d'après le nom.

Finir avec le contexte, c.-à-d. à la sortie du programme, il faut que le contexte ne soit plus d'actualité, puis le détruire.

if(!alcCall(alcMakeContextCurrent, contextMadeCurrent, openALDevice, nullptr))
{
    /* what can you do? */
}

if(!alcCall(alcDestroyContext, openALDevice, openALContext))
{
    /* not much you can do */
}

La seule erreur possible de alcDestroyContextest la même que celle de alcMakeContextCurrent- ALC_INVALID_CONTEXT; si vous faites tout correctement, alors vous ne l'obtiendrez pas, mais si vous le faites, alors rien ne peut être fait à ce sujet.

Pourquoi vérifier les erreurs avec lesquelles rien ne peut être fait?

Parce que je veux que les messages les concernant apparaissent au moins dans le flux d'erreurs, ce qui pour nous. alcCallSupposons qu'il ne nous donne jamais d'erreurs, mais il sera utile de savoir qu'une telle erreur se produit sur l'ordinateur de quelqu'un d'autre. Grâce à cela, nous pouvons étudier le problème, et éventuellement signaler un bug aux développeurs OpenAL Soft .

Jouez notre premier son


Eh bien, assez de tout cela, jouons le son. Pour commencer, nous avons évidemment besoin d'un fichier son. Par exemple, celui-ci, d'un jeu que je finirai jamais .


Je suis le protecteur de ce système!

Alors, ouvrez l'IDE et utilisez le code suivant. N'oubliez pas de brancher OpenAL Soft et d'ajouter le code de téléchargement de fichier et le code de vérification d'erreur ci-dessus.

int main()
{
    ALCdevice* openALDevice = alcOpenDevice(nullptr);
    if(!openALDevice)
        return 0;

    ALCcontext* openALContext;
    if(!alcCall(alcCreateContext, openALContext, openALDevice, openALDevice, nullptr) || !openALContext)
    {
        std::cerr << "ERROR: Could not create audio context" << std::endl;
        return 0;
    }
    ALCboolean contextMadeCurrent = false;
    if(!alcCall(alcMakeContextCurrent, contextMadeCurrent, openALDevice, openALContext)
       || contextMadeCurrent != ALC_TRUE)
    {
        std::cerr << "ERROR: Could not make audio context current" << std::endl;
        return 0;
    }

    std::uint8_t channels;
    std::int32_t sampleRate;
    std::uint8_t bitsPerSample;
    std::vector<char> soundData;
    if(!load_wav("iamtheprotectorofthissystem.wav", channels, sampleRate, bitsPerSample, soundData))
    {
        std::cerr << "ERROR: Could not load wav" << std::endl;
        return 0;
    }

    ALuint buffer;
    alCall(alGenBuffers, 1, &buffer);

    ALenum format;
    if(channels == 1 && bitsPerSample == 8)
        format = AL_FORMAT_MONO8;
    else if(channels == 1 && bitsPerSample == 16)
        format = AL_FORMAT_MONO16;
    else if(channels == 2 && bitsPerSample == 8)
        format = AL_FORMAT_STEREO8;
    else if(channels == 2 && bitsPerSample == 16)
        format = AL_FORMAT_STEREO16;
    else
    {
        std::cerr
            << "ERROR: unrecognised wave format: "
            << channels << " channels, "
            << bitsPerSample << " bps" << std::endl;
        return 0;
    }

    alCall(alBufferData, buffer, format, soundData.data(), soundData.size(), sampleRate);
    soundData.clear(); // erase the sound in RAM

    ALuint source;
    alCall(alGenSources, 1, &source);
    alCall(alSourcef, source, AL_PITCH, 1);
    alCall(alSourcef, source, AL_GAIN, 1.0f);
    alCall(alSource3f, source, AL_POSITION, 0, 0, 0);
    alCall(alSource3f, source, AL_VELOCITY, 0, 0, 0);
    alCall(alSourcei, source, AL_LOOPING, AL_FALSE);
    alCall(alSourcei, source, AL_BUFFER, buffer);

    alCall(alSourcePlay, source);

    ALint state = AL_PLAYING;

    while(state == AL_PLAYING)
    {
        alCall(alGetSourcei, source, AL_SOURCE_STATE, &state);
    }

    alCall(alDeleteSources, 1, &source);
    alCall(alDeleteBuffers, 1, &buffer);

    alcCall(alcMakeContextCurrent, contextMadeCurrent, openALDevice, nullptr);
    alcCall(alcDestroyContext, openALDevice, openALContext);

    ALCboolean closed;
    alcCall(alcCloseDevice, closed, openALDevice, openALDevice);

    return 0;
}

Compilation! Nous composons! Lancement! Je suis le pré-protecteur de ce système . Si vous n'entendez pas le son, vérifiez à nouveau tout. Si quelque chose est écrit dans la fenêtre de la console, cela devrait être la sortie standard du flux d'erreur, et c'est important. Nos fonctions de rapport d'erreurs devraient nous indiquer la ligne de code source qui a généré l'erreur. Si vous trouvez une

erreur, lisez le Guide du programmeur et la spécification pour comprendre les conditions dans lesquelles cette erreur peut être générée par une fonction. Cela vous aidera à le comprendre. Si cela ne réussit pas, laissez un commentaire sous l'article d'origine et j'essaierai de vous aider.

Télécharger les données RIFF WAVE


std::uint8_t channels;
std::int32_t sampleRate;
std::uint8_t bitsPerSample;
std::vector<char> soundData;
if(!load_wav("iamtheprotectorofthissystem.wav", channels, sampleRate, bitsPerSample, soundData))
{
    std::cerr << "ERROR: Could not load wav" << std::endl;
    return 0;
}

Il s'agit du code de démarrage Wave. L'important est que nous recevions des données, soit sous forme de pointeur, soit collectées dans un vecteur: le nombre de canaux, la fréquence d'échantillonnage et le nombre de bits par échantillon.

Génération de tampon


ALuint buffer;
alCall(alGenBuffers, 1, &buffer);

Cela vous semble probablement familier si vous avez déjà généré des tampons de données de texture dans OpenGL. En substance, nous générons un tampon et faisons comme s'il n'existait que sur la carte son. En fait, il sera très probablement stocké dans la RAM ordinaire, mais la spécification OpenAL résume toutes ces opérations.

Ainsi, la valeur ALuintest un handle vers notre tampon. N'oubliez pas que le tampon est essentiellement des données sonores dans la mémoire de la carte son. Nous n'avons plus accès directement à ces données, car nous les avons prises du programme (de la RAM ordinaire) et les avons déplacées vers une carte son / puce, etc. OpenGL fonctionne de manière similaire, en déplaçant les données de texture de la RAM vers la VRAM.

Ce descripteurgénère alGenBuffers. Il a quelques valeurs d'erreur possibles, dont la plus importante est AL_OUT_OF_MEMORY, ce qui signifie que nous ne pouvons plus ajouter de données audio à la carte son. Vous n'obtiendrez pas cette erreur si, par exemple, vous utilisez un seul tampon, mais vous devez en tenir compte si vous créez un moteur .

Déterminer le format des données audio


ALenum format;

if(channels == 1 && bitsPerSample == 8)
    format = AL_FORMAT_MONO8;
else if(channels == 1 && bitsPerSample == 16)
    format = AL_FORMAT_MONO16;
else if(channels == 2 && bitsPerSample == 8)
    format = AL_FORMAT_STEREO8;
else if(channels == 2 && bitsPerSample == 16)
    format = AL_FORMAT_STEREO16;
else
{
    std::cerr
        << "ERROR: unrecognised wave format: "
        << channels << " channels, "
        << bitsPerSample << " bps" << std::endl;
    return 0;
}

Les données sonores fonctionnent comme ceci: il y a plusieurs canaux et il y a une taille de bit par échantillon . Les données se composent de nombreux échantillons .

Pour déterminer le nombre d' échantillons dans les données audio, nous procédons comme suit:

std::int_fast32_t numberOfSamples = dataSize / (numberOfChannels * (bitsPerSample / 8));

Ce qui peut être facilement converti en calcul de la durée des données audio:

std::size_t duration = numberOfSamples / sampleRate;

Mais alors que nous n'avons pas besoin de savoir ni numberOfSamples, ni duration, cependant, il est important de savoir comment toutes ces informations sont utilisées.

Revenons à format- nous devons indiquer à OpenAL le format des données audio. Cela semble évident, non? Semblable à la façon dont nous remplissons le tampon de texture OpenGL, en disant que les données sont dans une séquence BGRA et sont composées de valeurs 8 bits, nous devons faire de même dans OpenAL.

Pour dire à OpenAL comment interpréter les données pointées par le pointeur que nous passerons plus tard, nous devons définir le format des données. Sous le format , il est censé être compris par OpenAL. Il n'y a que quatre significations possibles. Il existe deux valeurs possibles pour le nombre de canaux: une pour mono, deux pour stéréo.

En plus du nombre de canaux, nous avons le nombre de bits par échantillon. Il est égal à ou 8, ou 16, et est essentiellement de qualité sonore.

Ainsi, en utilisant les valeurs des canaux et des bits par échantillon, dont la fonction de charge d'onde nous a informés, nous pouvons déterminer celui à ALenumutiliser pour le futur paramètre format.

Remplissage de tampon


alCall(alBufferData, buffer, format, soundData.data(), soundData.size(), sampleRate);
soundData.clear(); // erase the sound in RAM

Avec cela, tout devrait être simple. Nous chargeons dans le tampon OpenAL, vers lequel pointe le descripteur buffer ; les données pointées par ptr soundData.data()dans la taille du sizespécifié sampleRate. Nous informerons également OpenAL du format de ces données via le paramètre format.

Au final, nous supprimons simplement les données reçues par le chargeur de vagues. Pourquoi? Parce que nous les avons déjà copiés sur la carte son. Nous n'avons pas besoin de les stocker à deux endroits et de dépenser de précieuses ressources. Si la carte son perd des données, nous la téléchargerons simplement à nouveau à partir du disque et nous n'aurons pas besoin de la copier sur le processeur ou sur quelqu'un d'autre.

Réglage de la source


Rappelons qu'OpenAL est essentiellement un auditeur qui écoute les sons émis par une ou plusieurs sources . Eh bien, il est maintenant temps de créer une source sonore.

ALuint source;
alCall(alGenSources, 1, &source);
alCall(alSourcef, source, AL_PITCH, 1);
alCall(alSourcef, source, AL_GAIN, 1.0f);
alCall(alSource3f, source, AL_POSITION, 0, 0, 0);
alCall(alSource3f, source, AL_VELOCITY, 0, 0, 0);
alCall(alSourcei, source, AL_LOOPING, AL_FALSE);
alCall(alSourcei, source, AL_BUFFER, buffer);

Honnêtement, il n'est pas nécessaire de définir certains de ces paramètres, car les valeurs par défaut nous conviennent parfaitement. Mais cela nous montre certains aspects avec lesquels vous pouvez expérimenter et voir ce qu'ils font (vous pouvez même agir habilement et les changer au fil du temps).

D'abord, nous générons la source - rappelez-vous, ceci est encore une fois une poignée vers quelque chose dans l'API OpenAL. Nous réglons la hauteur (ton) pour qu'elle ne change pas, le gain (volume) est égal à la valeur d'origine des données audio, la position et la vitesse sont réinitialisées; nous ne bouclons pas le son, sinon notre programme ne se terminera jamais et indiquera le buffer.

N'oubliez pas que différentes sources peuvent utiliser le même tampon. Par exemple, les ennemis tirant sur un joueur à différents endroits peuvent jouer le même son de tir, nous n'avons donc pas besoin de nombreuses copies des données sonores, mais seulement de quelques endroits dans l'espace 3D à partir duquel le son est produit.

Jouer son


alCall(alSourcePlay, source);

ALint state = AL_PLAYING;

while(state == AL_PLAYING)
{
    alCall(alGetSourcei, source, AL_SOURCE_STATE, &state);
}

Nous devons d'abord commencer à lire la source. Appelez simplement alSourcePlay.

Ensuite, nous créons une valeur pour stocker l'état actuel de la AL_SOURCE_STATEsource et le mettons à jour à l'infini. Quand il n'est plus égal, AL_PLAYINGnous pouvons continuer. Vous pouvez modifier l'état à la AL_STOPPEDfin de l'émission du son à partir du tampon (ou lorsqu'une erreur se produit). Si vous définissez la valeur de bouclage true, le son sera joué indéfiniment.

Ensuite, nous pouvons changer le tampon source et jouer un autre son. Ou rejouez le même son, etc. Réglez simplement le tampon, utilisez alSourcePlay-le et peut-être alSourceStop, si nécessaire. Dans les articles suivants, nous examinerons cela plus en détail.

Nettoyage


alCall(alDeleteSources, 1, &source);
alCall(alDeleteBuffers, 1, &buffer);

Comme nous lisons simplement les données audio une fois et que nous sortons, nous supprimerons la source et le tampon précédemment créés.

Le reste du code est compréhensible sans explication.

Où aller ensuite?


Connaissant tout ce qui est décrit dans cet article, vous pouvez déjà créer un petit jeu! Essayez de créer Pong ou un autre jeu classique , pour eux plus n'est pas nécessaire.

Mais rappelles-toi! Ces tampons ne conviennent qu'aux sons courts, très probablement pendant quelques secondes. Si vous avez besoin de musique ou de voix, vous devrez diffuser du son sur OpenAL. Nous en parlerons dans l'une des parties suivantes d'une série de tutoriels.

All Articles