Guia para trabalhar com o OpenAL em C ++. Parte 1: reproduzir som

Seu jogo precisa de som! Você provavelmente já usou o OpenGL para desenhar na tela. Você descobriu sua API e, portanto, recorreu ao OpenAL porque o nome parece familiar.

Bem, a boa notícia é que o OpenAL também possui uma API muito familiar. Foi originalmente projetado para simular a API de especificação OpenGL. Foi por isso que o escolhi entre os muitos sistemas de som para jogos; além disso, é multiplataforma.

Neste artigo, falarei em detalhes sobre qual código é necessário para usar o OpenAL em um jogo escrito em C ++. Discutiremos sons, músicas e posicionamento de sons no espaço 3D com exemplos de código.

História do OpenAL


Vou tentar ser breve. Como mencionado acima, ele foi intencionalmente projetado como uma imitação da API do OpenGL, e há uma razão para isso. Essa é uma API conveniente que é conhecida por muitos e, se os gráficos forem um dos lados do mecanismo do jogo, o som deverá ser diferente. Inicialmente, o OpenAL deveria ser de código aberto, mas algo aconteceu ...

As pessoas não estão tão interessadas em som quanto em gráficos, então a Creative acabou tornando o OpenAL sua propriedade, e a implementação de referência agora é proprietária e não é gratuita. Mas! A especificação OpenAL ainda é um padrão "aberto", ou seja, é publicada .

Periodicamente, as especificações são alteradas, mas não muitas. O som não muda tão rápido quanto os gráficos, porque não há necessidade específica disso.

A especificação aberta permitiu que outras pessoas criassem uma implementação de código aberto da especificação. Uma dessas implementações é o OpenAL Soft e, francamente, não faz sentido procurar outras. Esta é a implementação que vou usar e recomendo que você a use também.

Ela é multiplataforma. É implementado com muita curiosidade - na verdade, dentro da biblioteca, são usadas outras APIs de som presentes no seu sistema. No Windows, ele usa o DirectSound , no Unix, OSS . Graças a isso, ela conseguiu se tornar multiplataforma; Em essência, esse é um grande nome para a API do wrapper.

Você pode estar preocupado com a velocidade dessa API. Mas não se preocupe. Esse é o mesmo som e não cria uma carga grande; portanto, não requer as grandes otimizações exigidas pela API gráfica.

Mas já chega de história, vamos para a tecnologia.

O que você precisa para escrever código no OpenAL?


Você precisa criar o OpenAL Soft na cadeia de ferramentas de sua escolha. Este é um processo muito simples que você pode seguir de acordo com as instruções na seção Instalação de origem . Nunca tive problemas com isso, mas se você tiver alguma dificuldade, escreva um comentário no artigo original ou escreva para a lista de discussão do OpenAL Soft .

Em seguida, você precisará de alguns arquivos de som e uma maneira de baixá-los. O carregamento de dados de áudio em buffers e detalhes sutis de vários formatos de áudio estão fora do escopo deste artigo, mas você pode ler sobre o download e o streaming de arquivos Ogg / Vorbis . Baixar arquivos WAV é muito simples, já existem centenas de artigos na Internet sobre isso.

A tarefa de encontrar arquivos de áudio você precisa decidir por si mesmo. Existem muitos ruídos e explosões na Internet que você pode baixar. Se você tem um boato, pode tentar escrever sua própria música de chiptune [ tradução em Habré].

Além disso, mantenha o Guia do Programador do OpenALSoft à mão . Esta documentação é muito melhor em pdf com especialização "oficial".

Isso, de fato, é tudo. Assumiremos que você já sabe escrever código, usar o IDE e a cadeia de ferramentas.

Visão geral da API OpenAL


Como eu disse várias vezes, é semelhante à API do OpenGL. A semelhança está no fato de ser baseado em estados e você interagir com descritores / identificadores, e não com os próprios objetos diretamente.

Existem discrepâncias entre as convenções da API no OpenGL e OpenAL, mas elas não são significativas. No OpenGL, você precisa fazer chamadas especiais do sistema operacional para gerar um contexto de renderização. Estes desafios são diferentes para diferentes sistemas operacionais e não são realmente parte da especificação OpenGL. No OpenAL, tudo é diferente - as funções de criação de contexto fazem parte da especificação e são as mesmas, independentemente do sistema operacional.

Ao interagir com a API, existem três tipos principais de objetos com os quais você interage. Ouvintes("Ouvintes") é a localização dos "ouvidos" localizados no espaço 3D (sempre há apenas um ouvinte). Fontes ("fontes") são "alto-falantes" que produzem som, novamente no espaço 3D. O ouvinte e as fontes podem ser movidos no espaço e, dependendo disso, o que você ouve através dos alto-falantes do jogo muda.

Os últimos objetos são buffers . Eles armazenam amostras de sons que as fontes tocam para os ouvintes.

Existem também modos que o jogo usa para alterar a maneira como o áudio é processado através do OpenAL.

Fontes


Como mencionado acima, esses objetos são fontes de sons. Eles podem ser definidos como posição e direção e estão associados a um buffer de dados de áudio de reprodução.

Ouvinte


O único conjunto de "orelhas" no jogo. O que o ouvinte ouve é reproduzido pelos alto-falantes do computador. Ele também tem uma posição.

Buffers


No OpenGL, seu equivalente é Texture2D. Em essência, esses são os dados de áudio que a fonte reproduz.

Tipos de dados


Para poder oferecer suporte a código de plataforma cruzada, o OpenAL executa uma certa sequência de ações e define alguns tipos de dados. De fato, segue o OpenGL com tanta precisão que podemos converter diretamente tipos OpenAL em tipos OpenGL. A tabela abaixo lista eles e seus equivalentes.

Digite openalDigite openalcTipo openglC ++ TypedefDescrição
ALbooleanALCbooleanGLbooleanstd::int8_tValor booleano de 8 bits
ALbyteALCbyteGLbytestd::int8_tValor inteiro de 8 bits do código adicional com um sinal
ALubyteALCubyteGLubytestd::uint8_tValor inteiro não assinado de 8 bits
ALcharALCcharGLcharcharsímbolo
ALshortALCshortGLshortstd::int16_tValor inteiro assinado 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


Há um artigo sobre como simplificar o reconhecimento de erros OpenAL , mas por uma questão de integridade, repetirei aqui. Existem dois tipos de chamadas à API do OpenAL: regular e contextual.

As chamadas de contexto que começam com alcsão semelhantes às chamadas do OpenGL win32 para obter o contexto de renderização ou suas contrapartes no Linux. O som é bastante simples para todos os sistemas operacionais terem as mesmas chamadas. As chamadas comuns começam com al. Para obter erros em chamadas contextuais, chamamos alcGetError; no caso de ligações regulares, ligamos alGetError. Eles retornam um valor ALCenumou um valor ALenumque simplesmente lista possíveis erros.

Agora consideraremos apenas um caso, mas em todo o resto eles são quase os mesmos. Vamos assumir os desafios habituais al. Primeiro, crie uma macro de pré-processador para fazer o trabalho chato de passar detalhes:

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

Teoricamente, o compilador pode não suportar __FILE__tanto __LINE__, mas, para ser honesto, eu ficaria surpreso se isso acabou por ser assim. __VA_ARGS__indica um número variável de argumentos que podem ser passados ​​para essa macro.

Em seguida, implementamos uma função que recebe manualmente o último erro relatado e exibe um valor claro para o fluxo de erros padrão.

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

Não há muitos erros possíveis. As explicações que escrevi no código são as únicas informações que você receberá sobre esses erros, mas a especificação explica por que uma função específica pode retornar um erro específico.

Em seguida, implementamos duas funções de modelo diferentes que agrupam todas as nossas chamadas 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);
}

Existem dois deles, porque o primeiro é usado para funções OpenAL que retornam voide o segundo é usado quando a função retorna um valor não vazio. Se você não está familiarizado com os modelos de metaprogramação em C ++, consulte as partes do código c std::enable_if. Eles determinam quais dessas funções de modelo são implementadas pelo compilador para cada chamada de função.

E agora o mesmo para chamadas 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);
}

A maior mudança é a inclusão device, que todas as chamadas usam alc, bem como o uso correspondente de erros de estilo ALCenume ALC_. Eles parecem muito semelhantes, e por um tempo muito longo pequenas mudanças de alque alcmuito danificado meu código e compreensão, então eu continuei lendo direito em cima dela c.

Isso é tudo. Normalmente, uma chamada OpenAL em C ++ se parece com uma das seguintes opções:

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

Mas agora podemos fazer assim:

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

Pode parecer estranho para você, mas é mais conveniente para mim. Claro, você pode escolher uma estrutura diferente.

Baixar arquivos .wav


Você pode baixá-los você mesmo ou usar a biblioteca. Aqui está uma implementação de código aberto do carregamento de arquivos .wav . Eu sou louco, então eu mesmo faço:

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

Não explicarei o código, porque isso não está inteiramente no assunto do nosso artigo; mas é muito óbvio se você ler em paralelo com a especificação do arquivo WAV .

Inicialização e Destruição


Primeiro, precisamos inicializar o OpenAL e, como qualquer bom programador, finalizá-lo quando terminarmos de trabalhar com ele. É usado durante a inicialização ALCdevice(observe que nãoALC é ), o que representa essencialmente algo no seu computador para reproduzir música de fundo e a utiliza . semelhante à escolha de uma placa gráfica. no qual seu jogo OpenGL será renderizado. semelhante ao contexto de renderização que você deseja criar (exclusivo para o sistema operacional) para o OpenGL. ALALCcontext

ALCdeviceALCcontext

Alcdevice


Um dispositivo OpenAL é o que o som é transmitido, seja uma placa de som ou um chip, mas teoricamente pode haver muitas coisas diferentes. Semelhante a como a saída padrão iostreampode ser uma impressora em vez de uma tela, um dispositivo pode ser um arquivo ou mesmo um fluxo de dados.

No entanto, para jogos de programação, será um dispositivo de som e, geralmente, queremos que seja um dispositivo de saída de som padrão no sistema.

Para obter uma lista de dispositivos disponíveis no sistema, você pode solicitá-los com esta função:

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

Na verdade, é apenas um invólucro em torno de um invólucro em torno de uma chamada alcGetString. O valor de retorno é um ponteiro para uma lista de cadeias separadas por um valor nulle terminadas com dois valores null. Aqui, o invólucro simplesmente o transforma em um vetor conveniente para nós.

Felizmente, não precisamos fazer isso! No caso geral, como suspeito, a maioria dos jogos pode simplesmente emitir som para o dispositivo por padrão, seja ele qual for. Raramente vejo as opções para alterar o dispositivo de áudio através do qual você deseja emitir som. Portanto, para inicializar o dispositivo OpenAL, usamos uma chamada alcOpenDevice. Essa chamada é um pouco diferente de tudo o mais, porque não especifica o estado de erro que pode ser obtido alcGetError, por isso, chamamos isso de uma função normal:

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

Se você listou os dispositivos como mostrado acima, e deseja que o usuário selecione um deles, será necessário transferir o nome para alcOpenDeviceele nullptr. Enviar nullptrpedidos para abrir o dispositivo por padrão . O valor de retorno é o dispositivo correspondente ou nullptrse ocorrer um erro.

Dependendo se você completou a enumeração ou não, um erro pode parar o programa nas faixas. No device = No OpenAL; sem OpenAL = sem som; sem som = sem jogo.

A última coisa que fazemos ao fechar um programa é finalizá-lo corretamente.

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

Nesse estágio, se a conclusão não foi possível, isso não é mais importante para nós. Antes de fechar o dispositivo, devemos fechar todos os contextos criados; no entanto, na minha experiência, essa chamada também completa o contexto. Mas nós faremos isso direito. Se você concluir tudo antes de fazer uma chamada alcCloseDevice, não deverá haver erros e, por algum motivo, eles surgiram, não será possível fazer nada a respeito.

Você deve ter notado que as chamadas de alcCallenviam duas cópias do dispositivo. Isso aconteceu por causa de como a função de modelo funciona - uma é necessária para verificação de erros e a segunda é usada como parâmetro de função.

Teoricamente, posso melhorar a função de modelo para que ele passe o primeiro parâmetro para verificação de erros e ainda o envie para a função; mas tenho preguiça de fazê-lo. Vou deixar isso como sua lição de casa.

Nosso contexto da ALC


A segunda parte da inicialização é o contexto. Como antes, é semelhante ao contexto de renderização do OpenGL. Pode haver vários contextos em um programa e podemos alternar entre eles, mas não precisaremos disso. Cada contexto tem seu próprio ouvinte e fontes , e eles não podem ser transmitidos entre contextos.

Talvez isso seja útil em software de processamento de som. No entanto, para jogos em 99,9% dos casos, apenas um contexto é suficiente.

Criar um novo contexto é muito simples:

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

Precisamos nos comunicar para o ALCdeviceque queremos criar um contexto; também podemos passar uma lista opcional de chaves e valores ALCintque terminam com zero , que são atributos com os quais o contexto deve ser criado.

Honestamente, eu nem sei em que situação a passagem de atributos é útil. Seu jogo será executado em um computador comum com os recursos habituais de som. Os atributos têm valores padrão, dependendo do computador, portanto isso não é particularmente importante. Mas caso você ainda precise:

Nome do AtributoDescrição
ALC_FREQUENCYFrequência de mistura no buffer de saída, medida em Hz
ALC_REFRESHIntervalos de atualização, medidos em Hz
ALC_SYNC0ou 1indicar se deve ser um contexto síncrono ou assíncrono
ALC_MONO_SOURCESUm valor que ajuda a dizer quantas fontes você usará que exigem a capacidade de processar dados de áudio mono. Não limita a quantidade máxima, apenas permite que você seja mais eficaz quando você sabe disso com antecedência.
ALC_STEREO_SOURCESO mesmo, mas para dados estéreo.

Se você receber erros, provavelmente isso ocorre porque os atributos que você deseja são impossíveis ou você não pode criar outro contexto para o dispositivo suportado; isso resultará em um erro ALC_INVALID_VALUE. Se você passar um dispositivo inválido, receberá um erro ALC_INVALID_DEVICE, mas, é claro, já estamos verificando esse erro.

Criar contexto não é suficiente. Ainda precisamos atualizá-lo - parece um contexto de renderização do Windows OpenGL, certo? É o mesmo.

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

É necessário tornar o contexto atual para quaisquer operações adicionais com o contexto (ou com fontes e ouvintes nele). A operação retornará trueou false, o único valor de erro possível transmitido alcGetErroré aquele ALC_INVALID_CONTEXTque é claro no nome.

Finalizando com o contexto, ou seja, ao sair do programa, é necessário que o contexto não seja mais atual e destrua-o.

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

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

O único erro possível de alcDestroyContexté o mesmo de alcMakeContextCurrent- ALC_INVALID_CONTEXT; se você fizer tudo certo, não conseguirá, mas se fizer, nada poderá ser feito.

Por que verificar erros com os quais nada pode ser feito?

Porque quero que as mensagens sobre eles apareçam pelo menos no fluxo de erros, o que, para nós, alcCallsuponha que nunca nos dê erros, mas será útil saber que esse erro ocorre no computador de outra pessoa. Graças a isso, podemos estudar o problema e, possivelmente, reportar um bug aos desenvolvedores do OpenAL Soft .

Toque nosso primeiro som


Bem, chega de tudo isso, vamos tocar o som. Para começar, obviamente precisamos de um arquivo de som. Por exemplo, este, de um jogo que eu terminarei .


Eu sou o protetor deste sistema!

Então, abra o IDE e use o seguinte código. Lembre-se de conectar o OpenAL Soft e adicionar o código de upload do arquivo e o código de verificação de erros mostrado acima.

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! Nós compomos! Lançamento! Eu sou o prrrootetor deste sistema . Se você não ouvir o som, verifique tudo novamente. Se algo estiver escrito na janela do console, essa deve ser a saída padrão do fluxo de erros e isso é importante. Nossas funções de relatório de erros devem nos dizer a linha do código fonte que gerou o erro. Se você encontrar um

erro, leia o Guia do Programador e a especificação para entender as condições sob as quais esse erro pode ser gerado por uma função. Isso irá ajudá-lo a descobrir. Se não der certo, deixe um comentário no artigo original e tentarei ajudar.

Download dos dados do 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;
}

Isso se refere ao código de inicialização do wave. O importante é que recebamos dados, como um ponteiro, ou coletados em um vetor: o número de canais, a taxa de amostragem e o número de bits por amostra.

Geração de buffer


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

Provavelmente lhe parece familiar se você já gerou buffers de dados de textura no OpenGL. Em essência, geramos um buffer e fingimos que ele existirá apenas na placa de som. De fato, provavelmente será armazenado na RAM comum, mas a especificação OpenAL abstrai todas essas operações.

Portanto, o valor ALuinté um identificador para nosso buffer. Lembre-se de que o buffer é essencialmente um som na memória da placa de som. Não temos mais acesso direto a esses dados, pois os retiramos do programa (da RAM comum) e os movemos para uma placa de som / chip, etc. O OpenGL funciona de maneira semelhante, movendo dados de textura da RAM para a VRAM.

Este descritorgera alGenBuffers. Ele tem alguns possíveis valores de erro, o mais importante dos quais é AL_OUT_OF_MEMORY, o que significa que não podemos mais adicionar dados de som à placa de som. Você não receberá esse erro se, por exemplo, usar um único buffer, mas precisará considerá-lo se estiver criando um mecanismo .

Determinar o formato dos dados de áudio


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

Os dados de som funcionam assim: existem vários canais e há um tamanho de bit por amostra . Os dados consistem em muitas amostras .

Para determinar o número de amostras nos dados de áudio, fazemos o seguinte:

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

O que pode ser convenientemente convertido para calcular a duração dos dados de áudio:

std::size_t duration = numberOfSamples / sampleRate;

Mas, embora não precisemos saber numberOfSamplesnem duration, no entanto, é importante saber como todas essas informações são usadas.

Voltar para format- precisamos informar ao OpenAL o formato dos dados de áudio. Isso parece óbvio, certo? Semelhante à forma como preenchemos o buffer de textura do OpenGL, dizendo que os dados estão em uma sequência BGRA e são compostos por valores de 8 bits, precisamos fazer o mesmo no OpenAL.

Para dizer ao OpenAL como interpretar os dados apontados pelo ponteiro que passaremos adiante, precisamos definir o formato dos dados. Sob o formato , ele é entendido pelo OpenAL. Existem apenas quatro significados possíveis. Existem dois valores possíveis para o número de canais: um para mono e dois para estéreo.

Além do número de canais, temos o número de bits por amostra. É igual a ou 8, ou 16, e é essencialmente a qualidade do som.

Portanto, usando os valores dos canais e bits por amostra, sobre os quais a função de carga de onda nos informou, podemos determinar qual deles ALenumusar para o parâmetro futuro format.

Enchimento do tampão


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

Com isso, tudo deve ser simples. Carregamos no OpenAL Buffer, para o qual o descritor aponta buffer; os dados apontados por ptr soundData.data()no tamanho do sizeespecificado sampleRate. Nós também irá informar OpenAL o formato desses dados através do parâmetro format.

No final, simplesmente excluímos os dados que o carregador de ondas recebeu. Por quê? Porque nós já os copiamos para a placa de som. Não precisamos armazená-los em dois lugares e gastar recursos preciosos. Se a placa de som perder dados, basta fazer o download novamente do disco e não precisaremos copiá-la para a CPU ou outra pessoa.

Configuração de origem


Lembre-se de que o OpenAL é essencialmente um ouvinte que ouve sons emitidos por uma ou mais fontes . Bem, agora é a hora de criar uma fonte de som.

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, não é necessário definir alguns desses parâmetros, porque os valores padrão são bastante adequados para nós. Mas isso nos mostra alguns aspectos com os quais você pode experimentar e ver o que eles fazem (você pode até agir astuciosamente e alterá-los ao longo do tempo).

Primeiro, geramos código-fonte - lembre-se, isso é novamente um identificador para algo dentro da API OpenAL. Nós definimos o tom (tom) para que não mude, o ganho (volume) é igual ao valor original dos dados de áudio, a posição e a velocidade são redefinidas; nós não repetir o som, porque senão o nosso programa nunca vai acabar, e indicar o buffer.

Lembre-se de que fontes diferentes podem usar o mesmo buffer. Por exemplo, inimigos atirando em um jogador de lugares diferentes podem tocar o mesmo som de tiro, por isso não precisamos de muitas cópias dos dados do som, mas apenas de alguns lugares no espaço 3D a partir dos quais o som é produzido.

Tocar música


alCall(alSourcePlay, source);

ALint state = AL_PLAYING;

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

Primeiro, precisamos começar a reproduzir a fonte. Basta ligar alSourcePlay.

Em seguida, criamos um valor para armazenar o estado atual da AL_SOURCE_STATEfonte e atualizá-lo infinitamente. Quando não é mais igual, AL_PLAYINGpodemos continuar. Você pode alterar o estado para AL_STOPPEDquando terminar de emitir som do buffer (ou quando ocorrer um erro). Se você definir o valor do loop true, o som será reproduzido para sempre.

Então podemos mudar o buffer da fonte e tocar outro som. Ou reproduza o mesmo som, etc. Basta definir o buffer, usá- alSourcePlaylo e alSourceStop, talvez , se necessário. Nos seguintes artigos, consideraremos isso com mais detalhes.

Limpeza


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

Como simplesmente reproduzimos os dados de áudio uma vez e saímos, excluiremos a fonte e o buffer criados anteriormente.

O restante do código é compreensível sem explicação.

Para onde ir a seguir?


Sabendo tudo descrito neste artigo, você já pode criar um pequeno jogo! Tente criar Pong ou algum outro jogo clássico , para eles não é necessário mais.

Mas lembre-se! Esses buffers são adequados apenas para sons curtos, provavelmente por alguns segundos. Se você precisar de música ou dublagem, precisará transmitir áudio para o OpenAL. Falaremos sobre isso em uma das seguintes partes de uma série de tutoriais.

All Articles