Anleitung zum Arbeiten mit OpenAL in C ++. Teil 1: Ton abspielen

Dein Spiel braucht Sound! Sie haben OpenGL wahrscheinlich bereits zum Zeichnen auf dem Bildschirm verwendet. Sie haben die API herausgefunden und sich an OpenAL gewandt, weil Ihnen der Name bekannt vorkommt.

Die gute Nachricht ist, dass OpenAL auch eine sehr vertraute API hat. Es wurde ursprünglich entwickelt, um die OpenGL-Spezifikations-API zu simulieren. Deshalb habe ich es unter den vielen Soundsystemen für Spiele ausgewählt; Darüber hinaus ist es plattformübergreifend.

In diesem Artikel werde ich ausführlich darüber sprechen, welcher Code für die Verwendung von OpenAL in einem in C ++ geschriebenen Spiel benötigt wird. Wir werden Sounds, Musik und Soundpositionierung im 3D-Raum anhand von Codebeispielen diskutieren.

Geschichte von OpenAL


Ich werde versuchen, mich kurz zu fassen. Wie oben erwähnt, wurde es absichtlich als Nachahmung der OpenGL-API entwickelt, und es gibt einen Grund dafür. Dies ist eine praktische API, die vielen bekannt ist. Wenn die Grafiken eine Seite der Spiel-Engine sind, sollte der Sound anders sein. Ursprünglich sollte OpenAL Open Source sein, aber dann passierte etwas ... Die

Leute interessieren sich nicht so sehr für Sound wie für Grafiken, also machte Creative OpenAL schließlich zu seinem Eigentum, und die Referenzimplementierung ist jetzt proprietär und nicht kostenlos. Aber! Die OpenAL- Spezifikation ist immer noch ein „offener“ Standard, dh sie wird veröffentlicht .

Von Zeit zu Zeit werden Spezifikationen geändert, aber nicht viele. Der Sound ändert sich nicht so schnell wie die Grafik, da dies nicht besonders erforderlich ist.

Die offene Spezifikation ermöglichte es anderen Personen, eine Open-Source-Implementierung der Spezifikation zu erstellen. Eine solche Implementierung ist OpenAL Soft , und ehrlich gesagt macht es keinen Sinn, nach anderen zu suchen. Dies ist die Implementierung, die ich verwenden werde, und ich empfehle, dass Sie sie auch verwenden.

Sie ist plattformübergreifend. Es ist ziemlich merkwürdig implementiert - tatsächlich verwendet die Bibliothek andere Sound-APIs, die in Ihrem System vorhanden sind. Unter Windows wird DirectSound , unter Unix OSS verwendet . Dank dessen konnte sie plattformübergreifend werden; Im Wesentlichen ist dies ein großer Name für die Wrapper-API.

Möglicherweise sind Sie besorgt über die Geschwindigkeit dieser API. Aber mach dir keine Sorgen. Dies ist der gleiche Sound, und es wird keine große Last erstellt, sodass keine großen Optimierungen erforderlich sind, die für die Grafik-API erforderlich sind.

Aber genug Geschichte, gehen wir weiter zur Technologie.

Was benötigen Sie, um Code in OpenAL zu schreiben?


Sie müssen OpenAL Soft in der Toolchain Ihrer Wahl erstellen. Dies ist ein sehr einfacher Vorgang, den Sie gemäß den Anweisungen im Abschnitt Quellinstallation ausführen können . Ich hatte noch nie Probleme damit, aber wenn Sie irgendwelche Schwierigkeiten haben, schreiben Sie einen Kommentar unter den Originalartikel oder schreiben Sie an die OpenAL Soft Mailingliste .

Als nächstes benötigen Sie einige Audiodateien und eine Möglichkeit, sie herunterzuladen. Das Laden von Audiodaten in Puffer und subtile Details verschiedener Audioformate fallen nicht in den Geltungsbereich dieses Artikels. Sie können jedoch Informationen zum Herunterladen und Streamen von Ogg / Vorbis-Dateien lesen . Das Herunterladen von WAV-Dateien ist sehr einfach. Es gibt bereits Hunderte von Artikeln im Internet darüber.

Die Aufgabe, Audiodateien zu finden, müssen Sie selbst entscheiden. Es gibt viele Geräusche und Explosionen im Internet, die Sie herunterladen können. Wenn Sie ein Gerücht haben, können Sie versuchen , Ihre eigene Chiptune-Musik zu schreiben [ Übersetzung auf Habré].

Halten Sie außerdem das Programmierhandbuch von OpenALSoft bereit . Diese Dokumentation ist viel besser als PDF mit "offizieller" Spezialisierung.

Das ist in der Tat alles. Wir gehen davon aus, dass Sie bereits wissen, wie man Code schreibt, die IDE und die Toolchain verwendet.

OpenAL API Übersicht


Wie ich bereits mehrfach sagte, ähnelt es der OpenGL-API. Die Ähnlichkeit liegt in der Tatsache, dass es auf Zuständen basiert und Sie mit Deskriptoren / Bezeichnern interagieren und nicht direkt mit den Objekten selbst.

Es gibt Diskrepanzen zwischen den API-Konventionen in OpenGL und OpenAL, diese sind jedoch nicht signifikant. In OpenGL müssen Sie spezielle Betriebssystemaufrufe ausführen, um einen Renderkontext zu generieren. Diese Herausforderungen sind für verschiedene Betriebssysteme unterschiedlich und nicht wirklich Teil der OpenGL-Spezifikation. In OpenAL ist alles anders - die Funktionen zur Kontexterstellung sind Teil der Spezifikation und unabhängig vom Betriebssystem gleich.

Bei der Interaktion mit der API gibt es drei Haupttypen von Objekten, mit denen Sie interagieren. Zuhörer("Zuhörer") ist die Position der "Ohren" im 3D-Raum (es gibt immer nur einen Zuhörer). Quellen („Quellen“) sind „Lautsprecher“, die wiederum im 3D-Raum Ton erzeugen. Hörer und Quellen können im Raum bewegt werden und abhängig davon ändert sich das, was Sie über die Lautsprecher im Spiel hören.

Die letzten Objekte sind Puffer . Sie speichern Klangbeispiele, die Quellen für Hörer abspielen.

Es gibt auch Modi , mit denen das Spiel die Art und Weise ändert, wie Audio über OpenAL verarbeitet wird.

Quellen


Wie oben erwähnt, sind diese Objekte Schallquellen. Sie können in Position und Richtung eingestellt werden und sind einem Puffer für die Wiedergabe von Audiodaten zugeordnet.

Hörer


Der einzige Satz von "Ohren" im Spiel. Was der Hörer hört, wird über die Lautsprecher des Computers wiedergegeben. Er hat auch eine Position.

Puffer


In OpenGL entspricht dies Texture2D. Im Wesentlichen sind dies die Audiodaten, die von der Quelle reproduziert werden.

Datentypen


Um plattformübergreifenden Code unterstützen zu können, führt OpenAL eine bestimmte Abfolge von Aktionen aus und definiert einige Datentypen. Tatsächlich folgt es OpenGL so genau, dass wir OpenAL-Typen sogar direkt in OpenGL-Typen konvertieren können. In der folgenden Tabelle sind sie und ihre Entsprechungen aufgeführt.

Geben Sie openal einGeben Sie openalc einGeben Sie opengl einC ++ TypedefBeschreibung
ALbooleanALCbooleanGLbooleanstd::int8_t8-Bit-Boolescher Wert
ALbyteALCbyteGLbytestd::int8_t8-Bit-Ganzzahlwert des zusätzlichen Codes mit einem Vorzeichen
ALubyteALCubyteGLubytestd::uint8_t8-Bit-Ganzzahlwert ohne Vorzeichen
ALcharALCcharGLcharcharSymbol
ALshortALCshortGLshortstd::int16_t16-Bit-Ganzzahlwert mit Vorzeichen
ALushortALCushortGLushortstd::uint16_t16-
ALintALCintGLintstd::int32_t32-
ALuintALCuintGLuintstd::uint32_t32-
ALsizeiALCsizeiGLsizeistd::int32_t32-
ALenumALCenumGLenumstd::uint32_t32-
ALfloatALCfloatGLfloatfloat32- IEEE 754
ALdoubleALCdoubleGLdoubledouble64- IEEE 754
ALvoidALCvoidGLvoidvoid

OpenAL


Es gibt einen Artikel zur Vereinfachung der OpenAL-Fehlererkennung , der Vollständigkeit halber werde ich ihn hier wiederholen. Es gibt zwei Arten von OpenAL-API-Aufrufen: reguläre und kontextbezogene.

Kontextaufrufe, die mit beginnen, alcähneln OpenGL-Win32-Aufrufen, um den Renderkontext oder deren Gegenstücke unter Linux abzurufen. Sound ist so einfach, dass alle Betriebssysteme die gleichen Anrufe haben. Gewöhnliche Anrufe beginnen mit al. Um Fehler bei kontextbezogenen Aufrufen zu erhalten, rufen wir auf alcGetError. Bei regelmäßigen Anrufen rufen wir an alGetError. Sie geben entweder einen Wert ALCenumoder einen Wert zurück ALenum, der einfach mögliche Fehler auflistet.

Jetzt werden wir nur einen Fall betrachten, aber in allem anderen sind sie fast gleich. Nehmen wir die üblichen Herausforderungen an al. Erstellen Sie zunächst ein Präprozessor-Makro, um die langweilige Aufgabe des Übergebens von Details zu erledigen:

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

Theoretisch Compiler unterstützen können nicht __FILE__entweder __LINE__, aber, um ehrlich zu sein, würde ich überrascht sein , wenn das so entpuppt. __VA_ARGS__bezeichnet eine variable Anzahl von Argumenten, die an dieses Makro übergeben werden können.

Als Nächstes implementieren wir eine Funktion, die den zuletzt gemeldeten Fehler manuell empfängt und dem Standardfehlerstrom einen eindeutigen Wert anzeigt.

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

Es gibt nicht viele mögliche Fehler. Die Erklärungen, die ich im Code geschrieben habe, sind die einzigen Informationen, die Sie über diese Fehler erhalten. Die Spezifikation erklärt jedoch, warum eine bestimmte Funktion möglicherweise einen bestimmten Fehler zurückgibt.

Anschließend implementieren wir zwei verschiedene Vorlagenfunktionen, die alle unsere OpenGL-Aufrufe umschließen.

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

Es gibt zwei davon, da die erste für OpenAL-Funktionen verwendet wird, die zurückgeben void, und die zweite verwendet wird, wenn die Funktion einen nicht leeren Wert zurückgibt. Wenn Sie mit Metaprogrammiervorlagen in C ++ nicht sehr vertraut sind, schauen Sie sich die Teile des C-Codes an std::enable_if. Sie bestimmen für jeden Funktionsaufruf, welche dieser Vorlagenfunktionen vom Compiler implementiert werden.

Und jetzt das gleiche für Anrufe 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);
}

Die größte Änderung ist die Einbeziehung device, die alle Aufrufe verwenden alc, sowie die entsprechende Verwendung von Stilfehlern ALCenumund ALC_. Sie sehen sich sehr ähnlich und für eine sehr lange Zeit haben kleine Änderungen von alzu alcmeinen Code und mein Verständnis stark beschädigt, so dass ich einfach weiter darüber gelesen habe c.

Das ist alles. In der Regel sieht ein OpenAL-Aufruf in C ++ wie eine der folgenden Optionen aus:

/* 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 */
}

Aber jetzt können wir es so machen:

/* 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 */
}

Es mag Ihnen seltsam erscheinen, aber es ist bequemer für mich. Natürlich können Sie eine andere Struktur wählen.

Laden Sie WAV-Dateien herunter


Sie können sie entweder selbst herunterladen oder die Bibliothek verwenden. Hier ist eine Open-Source-Implementierung zum Laden von WAV-Dateien . Ich bin verrückt, also mache ich es selbst:

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

Ich werde den Code nicht erklären, da dies nicht vollständig Gegenstand unseres Artikels ist. Es ist jedoch sehr offensichtlich, wenn Sie es parallel zur Spezifikation der WAV-Datei lesen .

Initialisierung und Zerstörung


Zuerst müssen wir OpenAL initialisieren und dann, wie jeder gute Programmierer, beenden, wenn wir mit der Arbeit fertig sind. Es wird während der Initialisierung verwendet ALCdevice(beachten Sie, dass dies nicht der Fall ALCist ). Dies stellt im Wesentlichen etwas auf Ihrem Computer zum Abspielen von Hintergrundmusik dar und verwendet es . ähnlich wie bei der Auswahl einer Grafikkarte. auf dem Ihr OpenGL-Spiel gerendert wird. Ähnlich dem Rendering-Kontext, den Sie für OpenGL erstellen möchten (einzigartig für das Betriebssystem). ALALCcontext

ALCdeviceALCcontext

Alcdevice


Ein OpenAL-Gerät ist das, worüber der Sound ausgegeben wird, egal ob es sich um eine Soundkarte oder einen Chip handelt, aber theoretisch kann es viele verschiedene Dinge sein. Ähnlich wie die Standardausgabe iostreamein Drucker anstelle eines Bildschirms sein kann, kann ein Gerät eine Datei oder sogar ein Datenstrom sein.

Für die Programmierung von Spielen handelt es sich jedoch um ein Soundgerät, und normalerweise möchten wir, dass es sich um ein Standard-Soundausgabegerät im System handelt.

Um eine Liste der im System verfügbaren Geräte zu erhalten, können Sie diese mit folgender Funktion anfordern:

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

Dies ist eigentlich nur ein Wrapper um einen Wrapper um einen Anruf alcGetString. Der Rückgabewert ist ein Zeiger auf eine Liste von Zeichenfolgen, die durch einen Wert getrennt sind nullund mit zwei Werten enden null. Hier verwandelt der Wrapper ihn einfach in einen für uns geeigneten Vektor.

Zum Glück müssen wir das nicht tun! Im Allgemeinen können die meisten Spiele, wie ich vermute, standardmäßig einfach Sound auf das Gerät ausgeben, unabhängig davon, um was es sich handelt. Ich sehe selten die Optionen zum Ändern des Audiogeräts, über das Sie Ton ausgeben möchten. Um das OpenAL-Gerät zu initialisieren, verwenden wir daher einen Aufruf alcOpenDevice. Dieser Aufruf unterscheidet sich geringfügig von allem anderen, da er nicht den Fehlerstatus angibt, über den er abgerufen werden kann. Daher nennen alcGetErrorwir ihn wie eine normale Funktion:

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

Wenn Sie die Geräte wie oben gezeigt aufgelistet haben und der Benutzer eines davon auswählen soll, müssen Sie alcOpenDevicestattdessen dessen Namen übertragen nullptr. Senden von nullptrBefehlen zum Öffnen des Geräts standardmäßig . Der Rückgabewert ist entweder das entsprechende Gerät oder nullptrwenn ein Fehler auftritt.

Je nachdem, ob Sie die Aufzählung abgeschlossen haben oder nicht, kann ein Fehler das Programm auf Spuren stoppen. Kein Gerät = kein OpenAL; kein OpenAL = kein Ton; kein Ton = kein Spiel.

Das Letzte, was wir beim Schließen eines Programms tun, ist, es korrekt zu beenden.

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

Wenn zu diesem Zeitpunkt eine Fertigstellung nicht möglich war, ist dies für uns nicht mehr wichtig. Bevor wir das Gerät schließen, müssen wir alle erstellten Kontexte schließen. Meiner Erfahrung nach vervollständigt dieser Aufruf jedoch auch den Kontext. Aber wir werden es richtig machen. Wenn Sie alles abschließen, bevor Sie einen Anruf tätigen alcCloseDevice, sollten keine Fehler auftreten. Wenn diese aus irgendeinem Grund aufgetreten sind, können Sie nichts dagegen tun.

Möglicherweise haben Sie bemerkt, dass Anrufe von alcCallzwei Kopien des Geräts senden. Dies geschah aufgrund der Funktionsweise der Vorlagenfunktion - eine wird für die Fehlerprüfung benötigt und die zweite wird als Funktionsparameter verwendet.

Theoretisch kann ich die Vorlagenfunktion so verbessern, dass sie den ersten Parameter für die Fehlerprüfung übergibt und ihn dennoch an die Funktion sendet. aber ich bin faul, es zu tun. Ich werde dies als Ihre Hausaufgabe belassen.

Unser ALC-Kontext


Der zweite Teil der Initialisierung ist der Kontext. Nach wie vor ähnelt es dem Rendering-Kontext von OpenGL. Es kann mehrere Kontexte in einem Programm geben und wir können zwischen ihnen wechseln, aber wir werden dies nicht brauchen. Jeder Kontext hat seinen eigenen Listener und seine eigenen Quellen , und sie können nicht zwischen Kontexten übertragen werden.

Vielleicht ist dies in Soundverarbeitungssoftware nützlich. Für Spiele in 99,9% der Fälle reicht jedoch nur ein Kontext aus.

Das Erstellen eines neuen Kontexts ist sehr einfach:

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

Wir müssen kommunizieren ALCdevice, um einen Kontext zu schaffen. Wir können auch eine optionale Liste von Schlüsseln und Werten ALCintmit Nullende übergeben , die Attribute sind, mit denen der Kontext erstellt werden soll.

Ehrlich gesagt weiß ich nicht einmal, in welcher Situation das Übergeben von Attributen nützlich ist. Ihr Spiel läuft auf einem normalen Computer mit den üblichen Soundfunktionen. Attribute haben je nach Computer Standardwerte, daher ist dies nicht besonders wichtig. Aber falls Sie es noch brauchen:

AttributnameBeschreibung
ALC_FREQUENCYMischfrequenz zum Ausgangspuffer, gemessen in Hz
ALC_REFRESHAktualisierungsintervalle, gemessen in Hz
ALC_SYNC0oder 1geben Sie an, ob es sich um einen synchronen oder asynchronen Kontext handeln soll
ALC_MONO_SOURCESEin Wert, mit dem Sie feststellen können, wie viele Quellen Sie verwenden müssen, um monaurale Audiodaten verarbeiten zu können. Es begrenzt nicht die maximale Menge, es ermöglicht Ihnen nur, effektiver zu sein, wenn Sie dies im Voraus wissen.
ALC_STEREO_SOURCESDas gleiche, aber für Stereodaten.

Wenn Sie Fehler erhalten, liegt dies höchstwahrscheinlich daran, dass die gewünschten Attribute nicht möglich sind oder Sie keinen anderen Kontext für das unterstützte Gerät erstellen können. Dies führt zu einem Fehler ALC_INVALID_VALUE. Wenn Sie ein ungültiges Gerät übergeben, wird eine Fehlermeldung angezeigt, ALC_INVALID_DEVICEaber wir überprüfen diesen Fehler natürlich bereits.

Das Erstellen eines Kontexts reicht nicht aus. Wir müssen es noch aktuell machen - es sieht aus wie ein Windows OpenGL-Rendering-Kontext, oder? Es ist das Gleiche.

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 */
}

Es ist notwendig, den Kontext für alle weiteren Operationen mit dem Kontext (oder mit Quellen und Listenern darin) aktuell zu machen. Die Operation gibt zurück trueoder falseder einzig mögliche übertragene Fehlerwert alcGetErrorist der, ALC_INVALID_CONTEXTder aus dem Namen hervorgeht.

Beenden mit Kontext, d.h. Beim Beenden des Programms muss der Kontext nicht mehr aktuell sein und anschließend zerstört werden.

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

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

Der einzig mögliche Fehler von alcDestroyContextist der gleiche wie der von alcMakeContextCurrent- ALC_INVALID_CONTEXT; Wenn Sie alles richtig machen, werden Sie es nicht bekommen, aber wenn Sie es tun, können Sie nichts dagegen tun.

Warum nach Fehlern suchen, mit denen nichts gemacht werden kann?

Weil ich möchte, dass die Nachrichten darüber zumindest im Fehlerstrom erscheinen, was für uns der Fall ist. alcCallAngenommen, es gibt uns nie Fehler, aber es ist nützlich zu wissen, dass ein solcher Fehler auf dem Computer eines anderen auftritt. Dank dessen können wir das Problem untersuchen und möglicherweise einen Fehler den OpenAL Soft-Entwicklern melden .

Spielen Sie unseren ersten Sound


Nun, genug von all dem, lass uns den Sound spielen. Zunächst benötigen wir natürlich eine Sounddatei. Zum Beispiel dieses aus einem Spiel, das ich jemals beenden werde .


Ich bin der Beschützer dieses Systems!

Öffnen Sie also die IDE und verwenden Sie den folgenden Code. Denken Sie daran, OpenAL Soft anzuschließen und den oben gezeigten Code zum Hochladen von Dateien und Fehlercode hinzuzufügen.

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

Kompilieren! Wir komponieren! Starten! Ich bin der Vorreiter dieses Systems . Wenn Sie den Ton nicht hören, überprüfen Sie alles erneut. Wenn etwas in das Konsolenfenster geschrieben ist, sollte dies die Standardausgabe des Fehlerstroms sein, und dies ist wichtig. Unsere Fehlerberichterstattungsfunktionen sollten uns die Quellcodezeile mitteilen, die den Fehler generiert hat. Wenn Sie einen

Fehler finden, lesen Sie das Programmierhandbuch und die Spezifikation , um die Bedingungen zu verstehen, unter denen dieser Fehler von einer Funktion generiert werden kann. Dies wird Ihnen helfen, es herauszufinden. Wenn dies nicht gelingt, hinterlassen Sie einen Kommentar unter dem Originalartikel, und ich werde versuchen, Ihnen zu helfen.

Laden Sie die RIFF WAVE-Daten herunter


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

Dies bezieht sich auf den Wave-Boot-Code. Wichtig ist, dass wir Daten entweder als Zeiger empfangen oder in einem Vektor sammeln: die Anzahl der Kanäle, die Abtastrate und die Anzahl der Bits pro Abtastung.

Puffererzeugung


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

Es kommt Ihnen wahrscheinlich bekannt vor, wenn Sie jemals Texturdatenpuffer in OpenGL generiert haben. Im Wesentlichen generieren wir einen Puffer und geben vor, dass er nur in der Soundkarte vorhanden ist. Tatsächlich wird es höchstwahrscheinlich im normalen RAM gespeichert, aber die OpenAL-Spezifikation abstrahiert alle diese Operationen.

Der Wert ALuintist also ein Handle für unseren Puffer. Denken Sie daran, dass der Puffer im Wesentlichen Sounddaten im Speicher der Soundkarte sind. Wir haben keinen direkten Zugriff mehr auf diese Daten, da wir sie aus dem Programm (aus dem normalen RAM) genommen und auf eine Soundkarte / einen Chip usw. verschoben haben. OpenGL funktioniert ähnlich und verschiebt Texturdaten vom RAM zum VRAM.

Dieser Deskriptorerzeugt alGenBuffers. Es gibt einige mögliche Fehlerwerte, von denen der wichtigste ist AL_OUT_OF_MEMORY, was bedeutet, dass wir der Soundkarte keine Sounddaten mehr hinzufügen können. Dieser Fehler wird nicht angezeigt, wenn Sie beispielsweise einen einzelnen Puffer verwenden. Sie müssen dies jedoch berücksichtigen, wenn Sie eine Engine erstellen .

Bestimmen Sie das Format der Audiodaten


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

Sounddaten funktionieren folgendermaßen: Es gibt mehrere Kanäle und es gibt eine Bitgröße pro Sample . Daten bestehen aus vielen Proben .

Um die Anzahl der Samples in den Audiodaten zu bestimmen , gehen wir wie folgt vor:

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

Was kann bequem zur Berechnung der Dauer von Audiodaten konvertiert werden:

std::size_t duration = numberOfSamples / sampleRate;

Wir müssen zwar weder wissen numberOfSamples, noch durationist es wichtig zu wissen, wie all diese Informationen verwendet werden.

Zurück zu format- Wir müssen OpenAL das Audiodatenformat mitteilen. Das scheint offensichtlich, oder? Ähnlich wie wir den OpenGL-Texturpuffer füllen und sagen, dass sich die Daten in einer BGRA-Sequenz befinden und aus 8-Bit-Werten bestehen, müssen wir dasselbe in OpenAL tun.

Um OpenAL mitzuteilen, wie die Daten zu interpretieren sind, auf die der Zeiger zeigt, den wir später übergeben werden, müssen wir das Datenformat definieren. Unter dem Format versteht man OpenAL. Es gibt nur vier mögliche Bedeutungen. Es gibt zwei mögliche Werte für die Anzahl der Kanäle: einen für Mono und zwei für Stereo.

Zusätzlich zur Anzahl der Kanäle haben wir die Anzahl der Bits pro Abtastung. Es ist gleich oder 8, oder 16und ist im Wesentlichen Klangqualität.

Anhand der Werte der Kanäle und Bits pro Abtastung, über die uns die Wellenlastfunktion informiert hat, können wir bestimmen, welche ALenumfür den zukünftigen Parameter verwendet werden sollen format.

Pufferfüllung


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

Damit sollte alles einfach sein. Wir laden in den OpenAL-Puffer, auf den der Deskriptor zeigt buffer; die Daten auf den ptr zeigt soundData.data()in der Größe sizedes angegeben sampleRate. Wir werden OpenAL auch das Format dieser Daten über den Parameter mitteilen format.

Am Ende löschen wir einfach die Daten, die der Wave Loader empfangen hat. Warum? Weil wir sie bereits auf die Soundkarte kopiert haben. Wir müssen sie nicht an zwei Orten aufbewahren und wertvolle Ressourcen ausgeben. Wenn die Soundkarte Daten verliert, laden wir sie einfach erneut von der Festplatte herunter und müssen sie nicht auf die CPU oder eine andere Person kopieren.

Quelleneinstellung


Denken Sie daran, dass OpenAL im Wesentlichen ein Listener ist , der Sounds von einer oder mehreren Quellen hört . Nun ist es an der Zeit, eine Klangquelle zu erstellen.

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

Ehrlich gesagt ist es nicht erforderlich, einige dieser Parameter festzulegen, da die Standardwerte für uns durchaus geeignet sind. Dies zeigt uns jedoch einige Aspekte, mit denen Sie experimentieren und sehen können, was sie tun (Sie können sogar schlau handeln und sie im Laufe der Zeit ändern).

Zuerst generieren wir eine Quelle - denken Sie daran, dies ist wieder ein Handle für etwas in der OpenAL-API. Wir stellen die Tonhöhe (den Ton) so ein, dass sie sich nicht ändert. Die Verstärkung (Lautstärke) entspricht dem ursprünglichen Wert der Audiodaten. Position und Geschwindigkeit werden zurückgesetzt. Wir schleifen den Sound nicht, da unser Programm sonst niemals endet und den Puffer anzeigt.

Denken Sie daran, dass verschiedene Quellen denselben Puffer verwenden können. Zum Beispiel können Feinde, die einen Spieler von verschiedenen Orten aus schießen, denselben Schusston abspielen, sodass wir nicht viele Kopien der Tondaten benötigen, sondern nur wenige Orte im 3D-Raum, von denen aus der Ton erzeugt wird.

Ton abspielen


alCall(alSourcePlay, source);

ALint state = AL_PLAYING;

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

Zuerst müssen wir anfangen, Source zu spielen. Einfach anrufen alSourcePlay.

Anschließend erstellen wir einen Wert, um den aktuellen Status der AL_SOURCE_STATEQuelle zu speichern und endlos zu aktualisieren. Wenn es nicht mehr gleich ist, können AL_PLAYINGwir weitermachen. Sie können den Status ändern, AL_STOPPEDwenn der Ton aus dem Puffer beendet ist (oder wenn ein Fehler auftritt). Wenn Sie den Wert für das Looping festlegen true, wird der Sound für immer abgespielt.

Dann können wir den Quellpuffer ändern und einen anderen Sound abspielen. Oder spielen Sie den gleichen Sound usw. ab. Stellen Sie einfach den Puffer ein, verwenden alSourcePlaySie ihn und alSourceStopggf., falls erforderlich. In den folgenden Artikeln werden wir dies genauer betrachten.

Reinigung


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

Da wir die Audiodaten einfach einmal abspielen und beenden, löschen wir die zuvor erstellte Quelle und den Puffer.

Der Rest des Codes ist ohne Erklärung verständlich.

Wohin als nächstes?


Wenn Sie alles wissen, was in diesem Artikel beschrieben wird, können Sie bereits ein kleines Spiel erstellen! Versuchen Sie, Pong oder ein anderes klassisches Spiel zu erstellen , für das mehr nicht erforderlich ist.

Aber erinnere dich! Diese Puffer sind nur für kurze Töne geeignet, höchstwahrscheinlich für einige Sekunden. Wenn Sie Musik oder Sprachausgabe benötigen, müssen Sie Audio an OpenAL streamen. Wir werden darüber in einem der folgenden Teile einer Reihe von Tutorials sprechen.

All Articles