Guía para trabajar con OpenAL en C ++. Parte 1: reproducir sonido

¡Tu juego necesita sonido! Probablemente ya haya usado OpenGL para dibujar en la pantalla. Descubriste su API, por lo que recurriste a OpenAL porque el nombre parece familiar.

Bueno, la buena noticia es que OpenAL también tiene una API muy familiar. Originalmente fue diseñado para simular la API de especificación OpenGL. Por eso lo elegí entre los muchos sistemas de sonido para juegos; Además, es multiplataforma.

En este artículo, hablaré en detalle sobre qué código se necesita para usar OpenAL en un juego escrito en C ++. Discutiremos los sonidos, la música y el posicionamiento del sonido en el espacio 3D con ejemplos de código.

Historia de OpenAL


Intentaré ser breve. Como se mencionó anteriormente, fue diseñado intencionalmente como una imitación de la API de OpenGL, y hay una razón para esto. Esta es una API conveniente que muchos conocen, y si los gráficos son un lado del motor del juego, entonces el sonido debería ser diferente. Inicialmente, se suponía que OpenAL era de código abierto, pero luego sucedió algo ... La

gente no está tan interesada en el sonido como en los gráficos, por lo que Creative finalmente hizo de OpenAL su propiedad, y la implementación de referencia ahora es propietaria y no gratuita. ¡Pero! La especificación OpenAL sigue siendo un estándar "abierto", es decir, se publica .

De vez en cuando, se modifican las especificaciones, pero no muchas. El sonido no cambia tan rápido como los gráficos, porque no hay una necesidad particular de esto.

La especificación abierta permitió a otras personas crear una implementación de código abierto de la especificación. Una de estas implementaciones es OpenAL Soft y, francamente, no tiene sentido buscar otras. Esta es la implementación que usaré, y le recomiendo que también la use.

Ella es multiplataforma. Se implementa de manera bastante curiosa; de hecho, dentro de la biblioteca utiliza otras API de sonido presentes en su sistema. En Windows, usa DirectSound , en Unix, OSS . Gracias a esto, ella pudo convertirse en multiplataforma; en esencia, este es un gran nombre para la API del contenedor.

Puede que le preocupe la velocidad de esta API. Pero no te preocupes. Este es el mismo sonido, y no crea una gran carga, por lo que no requiere las grandes optimizaciones requeridas por la API de gráficos.

Pero suficiente historia, pasemos a la tecnología.

¿Qué necesitas para escribir código en OpenAL?


Necesita construir OpenAL Soft en la cadena de herramientas que elija. Este es un proceso muy simple que puede seguir de acuerdo con las instrucciones en la sección Instalación de origen . Nunca he tenido ningún problema con esto, pero si tiene alguna dificultad, escriba un comentario debajo del artículo original o escriba a la lista de correo de OpenAL Soft .

A continuación, necesitará algunos archivos de sonido y una forma de descargarlos. La carga de datos de audio en buffers y detalles sutiles de varios formatos de audio están fuera del alcance de este artículo, pero puede leer sobre la descarga y transmisión de archivos Ogg / Vorbis . Descargar archivos WAV es muy simple, ya hay cientos de artículos en Internet sobre esto.

La tarea de encontrar archivos de audio debe decidirla usted mismo. Hay muchos ruidos y explosiones en Internet que puede descargar. Si tienes un rumor, entonces puedes intentar escribir tu propia música chiptune [ traducción en Habré].

Además, tenga a mano la Guía de programadores de OpenALSoft . Esta documentación es mucho mejor pdf con especialización "oficial".

Eso, de hecho, es todo. Asumiremos que ya sabe cómo escribir código, usar el IDE y la cadena de herramientas.

Descripción general de la API de OpenAL


Como dije varias veces, es similar a la API de OpenGL. La similitud radica en el hecho de que se basa en estados e interactúa con descriptores / identificadores, y no con los objetos en sí directamente.

Existen discrepancias entre las convenciones de API en OpenGL y OpenAL, pero no son significativas. En OpenGL, debe realizar llamadas especiales del sistema operativo para generar un contexto de representación. Estos desafíos son diferentes para diferentes sistemas operativos y no son realmente parte de la especificación OpenGL. En OpenAL, todo es diferente: las funciones de creación de contexto son parte de la especificación y son las mismas independientemente del sistema operativo.

Al interactuar con la API, existen tres tipos principales de objetos con los que interactúa. Oyentes("Oyentes") es la ubicación de los "oídos" ubicados en el espacio 3D (siempre hay un solo oyente). Las fuentes ("fuentes") son "altavoces" que producen sonido, nuevamente en el espacio 3D. El oyente y las fuentes se pueden mover en el espacio y, dependiendo de esto, lo que escuchas a través de los altavoces en el juego cambia.

Los últimos objetos son buffers . Almacenan muestras de sonidos que las fuentes reproducirán para los oyentes.

También hay modos que el juego usa para cambiar la forma en que se procesa el audio a través de OpenAL.

Fuentes


Como se mencionó anteriormente, estos objetos son fuentes de sonidos. Se pueden establecer en posición y dirección, y están asociados con un búfer de datos de audio de reproducción.

Oyente


El único conjunto de "orejas" en el juego. Lo que escucha el oyente se reproduce a través de los altavoces de la computadora. Él también tiene un puesto.

Tampones


En OpenGL, su equivalente es Texture2D. En esencia, estos son los datos de audio que reproduce la fuente.

Tipos de datos


Para poder soportar código multiplataforma, OpenAL realiza una cierta secuencia de acciones y define algunos tipos de datos. De hecho, sigue a OpenGL con tanta precisión que incluso podemos convertir directamente tipos OpenAL a tipos OpenGL. La siguiente tabla los enumera a ellos y sus equivalentes.

Tipo abiertoTipo openalcTipo openglC ++ TypedefDescripción
ALbooleanALCbooleanGLbooleanstd::int8_tValor booleano de 8 bits
ALbyteALCbyteGLbytestd::int8_tValor entero de 8 bits del código adicional con un signo
ALubyteALCubyteGLubytestd::uint8_tValor entero sin signo de 8 bits
ALcharALCcharGLcharcharsímbolo
ALshortALCshortGLshortstd::int16_tValor entero con signo de 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


Hay un artículo sobre cómo simplificar el reconocimiento de errores de OpenAL , pero en aras de la exhaustividad, lo repetiré aquí. Hay dos tipos de llamadas OpenAL API: regular y contextual.

Las llamadas de contexto que comienzan alcson similares a las llamadas de OpenGL win32 para obtener el contexto de representación o sus contrapartes en Linux. El sonido es lo suficientemente simple para que todos los sistemas operativos tengan las mismas llamadas. Las llamadas ordinarias comienzan con al. Para obtener errores en las llamadas contextuales, llamamos alcGetError; en el caso de llamadas regulares, llamamos alGetError. Devuelven un valor ALCenumo un valor ALenumque simplemente enumera posibles errores.

Ahora consideraremos un solo caso, pero en todo lo demás son casi lo mismo. Tomemos los desafíos habituales al. Primero, cree una macro de preprocesador para hacer el aburrido trabajo de pasar detalles:

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

Teóricamente, es posible que su compilador tampoco __FILE__sea compatible __LINE__, pero, para ser honesto, me sorprendería si así fuera. __VA_ARGS__denota un número variable de argumentos que se pueden pasar a esta macro.

A continuación, implementamos una función que recibe manualmente el último error informado y muestra un valor claro para el flujo de error estándar.

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

No hay muchos posibles errores. Las explicaciones que escribí en el código son la única información que recibirá sobre estos errores, pero la especificación explica por qué una función en particular puede devolver un error específico.

Luego implementamos dos funciones de plantilla diferentes que envolverán todas nuestras llamadas 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);
}

Hay dos de ellos, porque el primero se usa para las funciones OpenAL que regresan void, y el segundo se usa cuando la función devuelve un valor no vacío. Si no está muy familiarizado con las plantillas de metaprogramación en C ++, eche un vistazo a las partes del código c std::enable_if. Determinan cuáles de estas funciones de plantilla implementa el compilador para cada llamada de función.

Y ahora lo mismo para las llamadas 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);
}

El mayor cambio es la inclusión device, que utilizan todas las llamadas alc, así como el uso correspondiente de errores de estilo ALCenumy ALC_. Se ven muy similares, y por un tiempo muy largo desde pequeños cambios ala alcmuy dañado mi código y comprensión, así que continué leyendo justo encima de ella c.

Eso es todo. Por lo general, una llamada de OpenAL en C ++ parece una de las siguientes opciones:

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

Pero ahora podemos hacerlo así:

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

Puede parecerle extraño, pero es más conveniente para mí. Por supuesto, puede elegir una estructura diferente.

Descargar archivos .wav


Puede descargarlos usted mismo o usar la biblioteca. Aquí hay una implementación de código abierto para cargar archivos .wav . Estoy loco, así que lo hago yo mismo:

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

No explicaré el código, porque esto no está enteramente en el tema de nuestro artículo; pero es muy obvio si lo lee en paralelo con la especificación del archivo WAV .

Inicialización y Destrucción


Primero necesitamos inicializar OpenAL, y luego, como cualquier buen programador, terminarlo cuando terminemos de trabajar con él. Se usa durante la inicialización ALCdevice(tenga en cuenta que esto noALC es así AL ), que esencialmente representa algo en su computadora para reproducir música de fondo y lo usa ALCcontext.

ALCdevicesimilar a elegir una tarjeta gráfica. en el que se renderizará tu juego OpenGL. ALCcontextsimilar al contexto de representación que desea crear (exclusivo del sistema operativo) para OpenGL.

Alcdevice


Un dispositivo OpenAL es a través del cual se emite el sonido, ya sea una tarjeta de sonido o un chip, pero en teoría puede ser muchas cosas diferentes. Al igual que la salida estándar iostreampuede ser una impresora en lugar de una pantalla, un dispositivo puede ser un archivo o incluso una secuencia de datos.

Sin embargo, para la programación de juegos, será un dispositivo de sonido, y generalmente queremos que sea un dispositivo de salida de sonido estándar en el sistema.

Para obtener una lista de dispositivos disponibles en el sistema, puede solicitarlos con esta función:

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

En realidad, esto es solo un contenedor alrededor de un contenedor alrededor de una llamada alcGetString. El valor de retorno es un puntero a una lista de cadenas separadas por un valor nully que termina con dos valores null. Aquí, el contenedor simplemente lo convierte en un vector conveniente para nosotros.

Afortunadamente, ¡no necesitamos hacer esto! En el caso general, como sospecho, la mayoría de los juegos pueden simplemente emitir sonido al dispositivo de forma predeterminada, sea lo que sea. Raramente veo las opciones para cambiar el dispositivo de audio a través del cual desea emitir sonido. Por lo tanto, para inicializar el dispositivo OpenAL, utilizamos una llamada alcOpenDevice. Esta llamada es ligeramente diferente de todo lo demás, porque no especifica el estado de error que se puede obtener alcGetError, por lo que lo llamamos como una función normal:

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

Si usted ha enumerado los dispositivos como se muestra arriba, y desea que el usuario seleccione uno de ellos, entonces usted necesita para transferir su nombre al alcOpenDevicelugar nullptr. Envío de nullptrórdenes para abrir el dispositivo por defecto . El valor de retorno es el dispositivo correspondiente o nullptrsi se produce un error.

Dependiendo de si ha completado la enumeración o no, un error puede detener el programa en las pistas. Sin dispositivo = Sin OpenAL; sin OpenAL = sin sonido; sin sonido = sin juego.

Lo último que hacemos al cerrar un programa es terminarlo correctamente.

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

En esta etapa, si la finalización no fue posible, entonces esto ya no es importante para nosotros. Antes de cerrar el dispositivo, debemos cerrar todos los contextos creados, sin embargo, en mi experiencia, esta llamada también completa el contexto. Pero lo haremos bien. Si completa todo antes de hacer una llamada alcCloseDevice, entonces no debe haber errores, y si por alguna razón han surgido, entonces no puede hacer nada al respecto.

Es posible que haya notado que las llamadas de alcCallenvían dos copias del dispositivo. Sucedió debido a cómo funciona la función de plantilla: una es necesaria para la verificación de errores y la segunda se usa como parámetro de función.

Teóricamente, puedo mejorar la función de la plantilla para que pase el primer parámetro para la verificación de errores y aún así la envíe a la función; pero soy flojo para hacerlo. Dejaré esto como tu tarea.

Nuestro contexto ALC


La segunda parte de la inicialización es el contexto. Como antes, es similar al contexto de representación de OpenGL. Puede haber varios contextos en un programa y podemos cambiar entre ellos, pero no lo necesitaremos. Cada contexto tiene su propio oyente y fuentes , y no se pueden pasar entre contextos.

Quizás esto sea útil en el software de procesamiento de sonido. Sin embargo, para los juegos en el 99.9% de los casos, solo un contexto es suficiente.

Crear un nuevo contexto es muy simple:

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

Necesitamos comunicarnos para lo ALCdeviceque queremos crear un contexto; También podemos pasar una lista opcional de claves y valores de finalización cero ALCint, que son atributos con los que se debe crear el contexto.

Honestamente, ni siquiera sé en qué situación es útil pasar el atributo. Tu juego se ejecutará en una computadora normal con las características de sonido habituales. Los atributos tienen valores predeterminados, según la computadora, por lo que esto no es particularmente importante. Pero en caso de que aún lo necesite:

Nombre del AtributoDescripción
ALC_FREQUENCYFrecuencia de mezcla al búfer de salida, medido en Hz
ALC_REFRESHIntervalos de actualización, medidos en Hz
ALC_SYNC0o 1indicar si debe ser un contexto síncrono o asíncrono
ALC_MONO_SOURCESUn valor que le ayuda a saber cuántas fuentes utilizará que requieren la capacidad de procesar datos de audio monoaural. No limita la cantidad máxima, solo le permite ser más efectivo cuando lo sabe de antemano.
ALC_STEREO_SOURCESLo mismo, pero para datos estéreo.

Si obtiene errores, lo más probable es que esto se deba a que los atributos que desea son imposibles o no puede crear otro contexto para el dispositivo compatible; Esto dará como resultado un error ALC_INVALID_VALUE. Si pasa un dispositivo no válido, recibirá un error ALC_INVALID_DEVICE, pero, por supuesto, ya estamos verificando este error.

Crear contexto no es suficiente. Todavía tenemos que actualizarlo: parece un contexto de representación de Windows OpenGL, ¿verdad? Es lo mismo.

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 necesario actualizar el contexto para cualquier otra operación con el contexto (o con fuentes y oyentes en él). La operación volverá trueo false, el único valor de error posible transmitido alcGetErrores el ALC_INVALID_CONTEXTque está claro del nombre.

Acabado con contexto, es decir al salir del programa, es necesario que el contexto ya no sea actual y luego destruirlo.

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

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

El único error posible de alcDestroyContextes el mismo que el de alcMakeContextCurrent- ALC_INVALID_CONTEXT; si haces todo bien, entonces no lo conseguirás, pero si lo haces, entonces no se puede hacer nada al respecto.

¿Por qué buscar errores con los que no se puede hacer nada?

Porque quiero que los mensajes sobre ellos aparezcan al menos en el flujo de errores, lo cual para nosotros sí. alcCallSupongamos que nunca nos da errores, pero será útil saber que tal error ocurre en la computadora de otra persona. Gracias a esto, podemos estudiar el problema y posiblemente informar un error a los desarrolladores de OpenAL Soft .

Toca nuestro primer sonido


Bueno, suficiente de todo esto, toquemos el sonido. Para empezar, obviamente necesitamos un archivo de sonido. Por ejemplo, este, de un juego que terminaré .


¡Soy el protector de este sistema!

Entonces, abra el IDE y use el siguiente código. Recuerde conectar OpenAL Soft y agregar el código de carga del archivo y el código de comprobación de errores que se muestra arriba.

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

Compilando! Nosotros componimos! ¡Lanzamiento! Soy el prrrootector de este sistema . Si no escucha el sonido, verifique todo nuevamente. Si algo está escrito en la ventana de la consola, esta debería ser la salida estándar de la secuencia de error, y es importante. Nuestras funciones de informe de errores deberían indicarnos la línea de código fuente que generó el error. Si encuentra un

error, lea la Guía del programador y la especificación para comprender las condiciones bajo las cuales este error puede ser generado por una función. Esto te ayudará a resolverlo. Si no tiene éxito, deje un comentario debajo del artículo original y trataré de ayudarlo.

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

Esto se refiere al código de arranque de onda. Lo importante es que recibamos datos, ya sea como puntero, o recopilados en un vector: el número de canales, la frecuencia de muestreo y el número de bits por muestra.

Generación de buffer


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

Probablemente le resulte familiar si alguna vez generó buffers de datos de textura en OpenGL. En esencia, generamos un búfer y pretendemos que solo existirá en la tarjeta de sonido. De hecho, lo más probable es que se almacene en la RAM ordinaria, pero la especificación OpenAL resume todas estas operaciones.

Entonces, el valor ALuintes un identificador de nuestro búfer. Recuerde que el búfer es esencialmente información de sonido en la memoria de la tarjeta de sonido. Ya no tenemos acceso directo a estos datos, ya que los tomamos del programa (de la RAM normal) y los trasladamos a una tarjeta de sonido / chip, etc. OpenGL funciona de manera similar, moviendo datos de textura de RAM a VRAM.

Este descriptorgenera alGenBuffers. Tiene un par de posibles valores de error, el más importante de los cuales es AL_OUT_OF_MEMORY, lo que significa que ya no podemos agregar datos de sonido a la tarjeta de sonido. No obtendrá este error si, por ejemplo, utiliza un único búfer, pero debe tenerlo en cuenta si está creando un motor .

Determinar el formato de los datos de 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;
}

Los datos de sonido funcionan así: hay varios canales y hay un tamaño de bit por muestra . Los datos consisten en muchas muestras .

Para determinar el número de muestras en los datos de audio, hacemos lo siguiente:

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

Lo que se puede convertir convenientemente para calcular la duración de los datos de audio:

std::size_t duration = numberOfSamples / sampleRate;

Pero si bien no necesitamos saberlo numberOfSamples, ni tampoco duration, sin embargo, es importante saber cómo se utilizan todos estos datos.

Volver a format- necesitamos decirle a OpenAL el formato de datos de audio. Eso parece obvio, ¿verdad? De manera similar a cómo llenamos el búfer de textura OpenGL, diciendo que los datos están en una secuencia BGRA y están compuestos de valores de 8 bits, necesitamos hacer lo mismo en OpenAL.

Para decirle a OpenAL cómo interpretar los datos señalados por el puntero que pasaremos más adelante, necesitamos definir el formato de datos. Bajo el formato , se entiende como lo entiende OpenAL. Solo hay cuatro significados posibles. Hay dos valores posibles para el número de canales: uno para mono, dos para estéreo.

Además del número de canales, tenemos el número de bits por muestra. Es igual a o 8, o 16, y es esencialmente calidad de sonido.

Entonces, utilizando los valores de los canales y bits por muestra, sobre los cuales la función de carga de onda nos ha informado, podemos determinar cuál ALenumusar para el parámetro futuro format.

Tampón de llenado


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

Con esto, todo debería ser simple. Cargamos en el OpenAL Buffer, al que apunta el descriptor buffer ; los datos apuntados por ptr soundData.data()en el tamaño del sizeespecificado sampleRate. También informaremos a OpenAL el formato de estos datos a través del parámetro format.

Al final, simplemente eliminamos los datos que recibió el cargador de ondas. ¿Por qué? Porque ya los hemos copiado en la tarjeta de sonido. No necesitamos almacenarlos en dos lugares y gastar recursos preciosos. Si la tarjeta de sonido pierde datos, simplemente la volveremos a descargar del disco y no necesitaremos copiarla a la CPU ni a otra persona.

Configuración de la fuente


Recuerde que OpenAL es esencialmente un oyente que escucha los sonidos hechos por una o más fuentes . Bueno, ahora es el momento de crear una fuente de sonido.

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

Honestamente, no es necesario establecer algunos de estos parámetros, porque los valores predeterminados son bastante adecuados para nosotros. Pero esto nos muestra algunos aspectos con los que puede experimentar y ver qué hacen (incluso puede actuar con astucia y cambiarlos con el tiempo).

Primero generamos fuente: recuerde, esto es nuevamente un identificador de algo dentro de la API de OpenAL. Configuramos el tono (tono) para que no cambie, la ganancia (volumen) se iguala al valor original de los datos de audio, la posición y la velocidad se restablecen; que no hacemos rizar el sonido, porque de lo contrario el programa no tendrá fin, e indicar el búfer.

Recuerde que diferentes fuentes pueden usar el mismo búfer. Por ejemplo, los enemigos que disparan a un jugador desde diferentes lugares pueden reproducir el mismo sonido de disparo, por lo que no necesitamos muchas copias de los datos de sonido, sino solo unos pocos lugares en el espacio 3D desde donde se produce el sonido.

Reproducir sonido


alCall(alSourcePlay, source);

ALint state = AL_PLAYING;

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

Primero tenemos que empezar a jugar fuente. Simplemente llama alSourcePlay.

Luego creamos un valor para almacenar el estado actual de la AL_SOURCE_STATEfuente y actualizarlo sin cesar. Cuando ya no es igual, AL_PLAYINGpodemos continuar. Puede cambiar el estado a AL_STOPPEDcuando termine de emitir sonido desde el búfer (o cuando ocurra un error). Si establece el valor de bucle true, el sonido se reproducirá para siempre.

Luego podemos cambiar el búfer de origen y reproducir otro sonido. O reproduce el mismo sonido, etc. Simplemente configure el búfer, úselo alSourcePlayy tal vez alSourceStop, si es necesario. En los siguientes artículos consideraremos esto con más detalle.

Limpieza


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

Como simplemente reproducimos los datos de audio una vez y salimos, eliminaremos la fuente y el búfer creados previamente.

El resto del código es comprensible sin explicación.

¿A dónde ir después?


¡Sabiendo todo lo descrito en este artículo, ya puedes crear un pequeño juego! Intenta crear Pong o algún otro juego clásico , para ellos no se requiere más.

¡Pero recuerda! Estos buffers solo son adecuados para sonidos cortos, muy probablemente por unos segundos. Si necesita música o actuación de voz, deberá transmitir audio a OpenAL. Hablaremos de esto en una de las siguientes partes de una serie de tutoriales.

All Articles