Panduan untuk bekerja dengan OpenAL di C ++. Bagian 1: mainkan suara

Game Anda membutuhkan suara! Anda mungkin sudah menggunakan OpenGL untuk menggambar di layar. Anda mengetahui API-nya, dan Anda beralih ke OpenAL karena nama itu tampak familier.

Nah, kabar baiknya adalah bahwa OpenAL juga memiliki API yang sangat akrab. Awalnya dirancang untuk mensimulasikan API spesifikasi OpenGL. Itulah sebabnya saya memilihnya di antara banyak sistem suara untuk game; selain itu, ini adalah cross-platform.

Pada artikel ini, saya akan berbicara secara rinci tentang kode apa yang diperlukan untuk menggunakan OpenAL dalam game yang ditulis dalam C ++. Kami akan membahas suara, musik, dan posisi suara dalam ruang 3D dengan contoh kode.

Sejarah OpenAL


Saya akan mencoba untuk singkat. Seperti disebutkan di atas, itu sengaja dirancang sebagai tiruan dari OpenGL API, dan ada alasan untuk ini. Ini adalah API yang nyaman yang dikenal banyak orang, dan jika grafiknya satu sisi dari mesin game, maka suaranya harus berbeda. Awalnya, OpenAL seharusnya open-source, tetapi kemudian sesuatu terjadi ...

Orang-orang tidak begitu tertarik pada suara seperti grafik, jadi Kreatif akhirnya menjadikan OpenAL sebagai propertinya, dan implementasi referensi sekarang menjadi milik dan tidak gratis. Tapi! Spesifikasi OpenAL masih merupakan standar "terbuka", yaitu diterbitkan .

Dari waktu ke waktu, spesifikasi diubah, tetapi tidak banyak. Suara tidak berubah secepat grafik, karena tidak ada kebutuhan khusus untuk ini.

Spesifikasi terbuka memungkinkan orang lain untuk membuat implementasi spesifikasi sumber terbuka. Salah satu implementasi tersebut adalah OpenAL Soft , dan terus terang, tidak masuk akal untuk mencari yang lain. Ini adalah implementasi yang akan saya gunakan, dan saya sarankan Anda menggunakannya juga.

Dia adalah lintas platform. Ini diterapkan dengan cukup aneh - pada kenyataannya, di dalam perpustakaan menggunakan API suara lain yang ada di sistem Anda. Di Windows, ia menggunakan DirectSound , di Unix, OSS . Berkat ini, dia bisa menjadi cross-platform; pada dasarnya, ini adalah nama besar untuk API pembungkus.

Anda mungkin khawatir tentang kecepatan API ini. Tapi jangan khawatir. Ini adalah suara yang sama, dan tidak menghasilkan beban yang besar, sehingga tidak memerlukan optimisasi besar yang diperlukan oleh API grafik.

Tapi cukup cerita, mari kita beralih ke teknologi.

Apa yang Anda butuhkan untuk menulis kode di OpenAL?


Anda perlu membuat OpenAL Soft di toolchain pilihan Anda. Ini adalah proses yang sangat sederhana yang dapat Anda ikuti sesuai dengan instruksi di bagian Instalasi Sumber . Saya tidak pernah memiliki masalah dengan ini, tetapi jika Anda memiliki kesulitan, tulis komentar di bawah artikel asli atau tulis ke milis OpenAL Soft .

Selanjutnya, Anda akan memerlukan beberapa file suara dan cara untuk mengunduhnya. Memuat data audio ke dalam buffer dan detail halus dari berbagai format audio berada di luar ruang lingkup artikel ini, tetapi Anda dapat membaca tentang mengunduh dan streaming file Ogg / Vorbis . Mengunduh file WAV sangat sederhana, sudah ada ratusan artikel di Internet tentang ini.

Tugas menemukan file audio Anda harus memutuskan sendiri. Ada banyak suara dan ledakan di Internet yang bisa Anda unduh. Jika Anda memiliki rumor, maka Anda dapat mencoba untuk menulis musik chiptune Anda sendiri [ terjemahan di Habré].

Juga, jaga Panduan Programmer dari OpenALSoft berguna . Dokumentasi ini adalah pdf yang jauh lebih baik dengan spesialisasi "resmi".

Faktanya, itu saja. Kami akan menganggap bahwa Anda sudah tahu cara menulis kode, menggunakan IDE dan toolchain.

Ikhtisar OpenAL API


Seperti yang saya katakan beberapa kali, ini mirip dengan OpenGL API. Kesamaan terletak pada kenyataan bahwa itu didasarkan pada keadaan dan Anda berinteraksi dengan deskriptor / pengidentifikasi, dan bukan dengan objek itu sendiri secara langsung.

Ada perbedaan antara konvensi API di OpenGL dan OpenAL, tetapi tidak signifikan. Di OpenGL, Anda perlu membuat panggilan OS khusus untuk menghasilkan konteks rendering. Tantangan-tantangan ini berbeda untuk berbagai OS dan tidak benar-benar bagian dari spesifikasi OpenGL. Dalam OpenAL, semuanya berbeda - fungsi pembuatan konteks adalah bagian dari spesifikasi dan sama terlepas dari sistem operasi.

Saat berinteraksi dengan API, ada tiga jenis objek utama yang berinteraksi dengan Anda. Pendengar("Pendengar") adalah lokasi "telinga" yang terletak di ruang 3D (selalu ada hanya satu pendengar). Sumber ("sumber") adalah "speaker" yang menghasilkan suara, sekali lagi dalam ruang 3D. Pendengar dan sumber dapat dipindahkan dalam ruang dan tergantung pada ini, apa yang Anda dengar melalui speaker dalam permainan berubah.

Objek terakhir adalah buffer . Mereka menyimpan sampel suara yang akan diputar sumber untuk pendengar.

Ada juga mode yang digunakan gim untuk mengubah cara audio diproses melalui OpenAL.

Sumber


Seperti disebutkan di atas, objek-objek ini adalah sumber suara. Mereka dapat mengatur posisi dan arah, dan mereka terkait dengan buffer data audio pemutaran.

Pendengar


Satu-satunya set "telinga" dalam game. Apa yang didengar pendengar direproduksi melalui pengeras suara komputer. Dia juga punya posisi.

Buffer


Di OpenGL, padanannya adalah Texture2D. Intinya, ini adalah data audio yang direproduksi oleh sumber.

Tipe data


Untuk dapat mendukung kode lintas platform, OpenAL melakukan urutan tindakan tertentu dan mendefinisikan beberapa tipe data. Bahkan, ia mengikuti OpenGL sehingga kita bahkan dapat langsung mengkonversi tipe OpenAL ke tipe OpenGL. Tabel di bawah mencantumkan mereka dan padanannya.

Ketikkan openalKetik openalcKetik openglC ++ TypedefDeskripsi
ALbooleanALCbooleanGLbooleanstd::int8_tNilai boolean 8-bit
ALbyteALCbyteGLbytestd::int8_tNilai integer 8-bit dari kode tambahan dengan tanda
ALubyteALCubyteGLubytestd::uint8_tNilai integer 8-bit yang tidak ditandatangani
ALcharALCcharGLcharcharsimbol
ALshortALCshortGLshortstd::int16_tNilai integer bertanda 16-bit
ALushortALCushortGLushortstd::uint16_t16-
ALintALCintGLintstd::int32_t32-
ALuintALCuintGLuintstd::uint32_t32-
ALsizeiALCsizeiGLsizeistd::int32_t32-
ALenumALCenumGLenumstd::uint32_t32-
ALfloatALCfloatGLfloatfloat32- IEEE 754
ALdoubleALCdoubleGLdoubledouble64- IEEE 754
ALvoidALCvoidGLvoidvoid

OpenAL


Ada sebuah artikel tentang bagaimana menyederhanakan pengenalan kesalahan OpenAL , tetapi demi kelengkapan, saya akan mengulanginya di sini. Ada dua jenis panggilan API OpenAL: reguler dan kontekstual.

Panggilan konteks dimulai dengan alcmirip dengan panggilan OpenGL win32 untuk mendapatkan konteks rendering atau rekan-rekan mereka di Linux. Suara adalah hal yang cukup sederhana untuk semua sistem operasi memiliki panggilan yang sama. Panggilan biasa dimulai dengan al. Untuk mendapatkan kesalahan dalam panggilan kontekstual, kami memanggil alcGetError; dalam hal panggilan biasa, kami menelepon alGetError. Mereka mengembalikan nilai ALCenumatau nilai ALenumyang hanya daftar kemungkinan kesalahan.

Sekarang kita akan mempertimbangkan hanya satu kasus, tetapi dalam segala hal lainnya mereka hampir sama. Mari kita ambil tantangan yang biasa al. Pertama, buat makro preprocessor untuk melakukan pekerjaan membosankan dengan melewatkan detail:

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

Secara teoritis, compiler Anda mungkin tidak mendukung __FILE__baik __LINE__, tapi, jujur, aku akan terkejut jika itu ternyata begitu. __VA_ARGS__menunjukkan sejumlah variabel argumen yang dapat diteruskan ke makro ini.

Selanjutnya, kami menerapkan fungsi yang secara manual menerima kesalahan terakhir yang dilaporkan dan menampilkan nilai yang jelas ke aliran kesalahan standar.

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

Tidak banyak kesalahan yang mungkin terjadi. Penjelasan yang saya tulis dalam kode adalah satu-satunya informasi yang akan Anda terima tentang kesalahan ini, tetapi spesifikasi menjelaskan mengapa fungsi tertentu dapat mengembalikan kesalahan tertentu.

Kemudian kami menerapkan dua fungsi templat berbeda yang akan membungkus semua panggilan OpenGL kami.

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

Ada dua di antaranya, karena yang pertama digunakan untuk fungsi OpenAL yang kembali void, dan yang kedua digunakan ketika fungsi mengembalikan nilai yang tidak kosong. Jika Anda tidak terlalu familiar dengan templat metaprogramming di C ++, lihat bagian-bagian dari kode c std::enable_if. Mereka menentukan fungsi templat mana yang diimplementasikan oleh kompiler untuk setiap panggilan fungsi.

Dan sekarang sama untuk panggilan 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);
}

Perubahan terbesar adalah penyertaan device, yang digunakan semua panggilan alc, serta penggunaan kesalahan gaya ALCenumdan ALC_. Mereka terlihat sangat mirip, dan untuk waktu yang sangat lama perubahan kecil dari alke alcrusak parah kode saya dan pemahaman, jadi aku hanya terus membaca tepat di atas itu c.

Itu saja. Biasanya, panggilan OpenAL di C ++ terlihat seperti salah satu opsi berikut:

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

Tapi sekarang kita bisa melakukannya seperti ini:

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

Ini mungkin terlihat aneh bagi Anda, tetapi lebih nyaman bagi saya. Tentu saja, Anda dapat memilih struktur yang berbeda.

Unduh file .wav


Anda dapat mengunduhnya sendiri, atau menggunakan perpustakaan. Berikut ini adalah implementasi open-source untuk memuat file .wav . Saya gila, jadi saya melakukannya sendiri:

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

Saya tidak akan menjelaskan kodenya, karena ini tidak sepenuhnya dalam subjek artikel kami; tetapi sangat jelas jika Anda membacanya secara paralel dengan spesifikasi file WAV .

Inisialisasi dan Penghancuran


Pertama kita perlu menginisialisasi OpenAL, dan kemudian, seperti programmer yang baik, selesaikan ketika kita selesai bekerja dengannya. Hal ini digunakan selama inisialisasi ALCdevice(catatan bahwa ini ALCadalah tidak AL ), yang pada dasarnya merupakan sesuatu pada komputer Anda untuk bermain musik dan menggunakan itu latar belakang ALCcontext.

ALCdevicemirip dengan memilih kartu grafis. di mana game OpenGL Anda akan ditampilkan. ALCcontextmirip dengan konteks render yang ingin Anda buat (unik untuk sistem operasi) untuk OpenGL.

Alcdevice


Perangkat OpenAL adalah apa suara keluar melalui, apakah itu kartu suara atau chip, tetapi secara teoritis itu bisa menjadi banyak hal yang berbeda. Mirip dengan bagaimana output standar iostreamdapat menjadi printer, bukan layar, perangkat dapat berupa file atau bahkan aliran data.

Namun, untuk permainan pemrograman, ini akan menjadi perangkat suara, dan biasanya kami ingin itu menjadi perangkat output suara standar dalam sistem.

Untuk mendapatkan daftar perangkat yang tersedia di sistem, Anda dapat meminta mereka dengan fungsi ini:

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

Ini sebenarnya hanya pembungkus pembungkus panggilan alcGetString. Nilai kembali adalah penunjuk ke daftar string yang dipisahkan oleh nilai nulldan diakhiri dengan dua nilai null. Di sini, pembungkus hanya mengubahnya menjadi vektor yang nyaman bagi kami.

Untungnya, kita tidak perlu melakukan ini! Dalam kasus umum, seperti yang saya duga, sebagian besar game hanya bisa mengeluarkan suara ke perangkat secara default, apa pun itu. Saya jarang melihat opsi untuk mengubah perangkat audio di mana Anda ingin menghasilkan suara. Karena itu, untuk menginisialisasi Perangkat OpenAL, kami menggunakan panggilan alcOpenDevice. Panggilan ini sedikit berbeda dari yang lainnya, karena tidak menentukan status kesalahan yang dapat diperoleh alcGetError, jadi kami menyebutnya seperti fungsi normal:

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

Jika Anda telah terdaftar perangkat seperti yang ditunjukkan di atas, dan Anda ingin pengguna untuk memilih salah satu dari mereka, maka Anda perlu untuk mentransfer nama menjadi alcOpenDevicegantinya nullptr. Mengirim nullptrpesanan untuk membuka perangkat secara default . Nilai kembali adalah perangkat yang sesuai, atau nullptrjika terjadi kesalahan.

Bergantung pada apakah Anda telah menyelesaikan enumerasi atau tidak, kesalahan dapat menghentikan program pada trek. No device = No OpenAL; tidak ada OpenAL = tidak ada suara; tidak ada suara = tidak ada permainan.

Hal terakhir yang kami lakukan ketika menutup program adalah menyelesaikannya dengan benar.

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

Pada tahap ini, jika penyelesaian tidak memungkinkan, maka ini tidak lagi penting bagi kami. Sebelum menutup perangkat, kita harus menutup semua konteks yang dibuat, namun, menurut pengalaman saya, panggilan ini juga melengkapi konteks. Tapi kami akan melakukannya dengan benar. Jika Anda menyelesaikan semua sebelum melakukan panggilan alcCloseDevice, maka seharusnya tidak ada kesalahan, dan jika karena alasan tertentu mereka muncul, maka Anda tidak dapat melakukan apa-apa.

Anda mungkin telah memperhatikan bahwa panggilan dari alcCallmengirim dua salinan perangkat. Itu terjadi karena cara fungsi templat bekerja - satu diperlukan untuk pengecekan kesalahan, dan yang kedua digunakan sebagai parameter fungsi.

Secara teoritis, saya dapat meningkatkan fungsi templat sehingga melewati parameter pertama untuk pengecekan kesalahan dan tetap mengirimkannya ke fungsi; tapi aku malas melakukannya. Saya akan meninggalkan ini sebagai pekerjaan rumah Anda.

Konteks ALC kami


Bagian kedua dari inisialisasi adalah konteks. Seperti sebelumnya, ini mirip dengan konteks render dari OpenGL. Mungkin ada beberapa konteks dalam satu program dan kita dapat beralih di antara mereka, tetapi kita tidak membutuhkan ini. Setiap konteks memiliki pendengar dan sumbernya sendiri , dan mereka tidak dapat dilewatkan di antara konteks.

Mungkin ini berguna dalam perangkat lunak pengolah suara. Namun, untuk game dalam 99,9% kasus, hanya satu konteks yang cukup.

Membuat konteks baru sangat sederhana:

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

Kita perlu berkomunikasi untuk apa yang ALCdeviceingin kita ciptakan konteks; kita juga dapat memberikan daftar pilihan kunci dan nilai ALCintyang diakhiri null , yang merupakan atribut yang dengannya konteks harus dibuat.

Jujur, saya bahkan tidak tahu dalam situasi apa atribut passing berguna. Game Anda akan berjalan di komputer biasa dengan fitur suara biasa. Atribut memiliki nilai default, tergantung pada komputer, jadi ini tidak terlalu penting. Tetapi jika Anda masih membutuhkannya:

Nama atributDeskripsi
ALC_FREQUENCYFrekuensi pencampuran ke buffer output, diukur dalam Hz
ALC_REFRESHInterval pembaruan, diukur dalam Hz
ALC_SYNC0atau 1menunjukkan apakah itu harus konteks yang sinkron atau asinkron
ALC_MONO_SOURCESNilai yang membantu memberi tahu Anda berapa banyak sumber yang akan Anda gunakan yang membutuhkan kemampuan untuk memproses data audio monaural. Itu tidak membatasi jumlah maksimum, itu hanya memungkinkan Anda untuk menjadi lebih efektif ketika Anda mengetahui hal ini sebelumnya.
ALC_STEREO_SOURCESSama, tetapi untuk data stereo.

Jika Anda mendapatkan kesalahan, kemungkinan besar ini karena atribut yang Anda inginkan tidak mungkin atau Anda tidak dapat membuat konteks lain untuk perangkat yang didukung; ini akan menghasilkan kesalahan ALC_INVALID_VALUE. Jika Anda melewati perangkat yang tidak valid, Anda akan mendapatkan kesalahan ALC_INVALID_DEVICE, tetapi, tentu saja, kami sudah memeriksa kesalahan ini.

Menciptakan konteks tidak cukup. Kita masih harus membuatnya tetap terkini - sepertinya Windows OpenGL Rendering Context, kan? Sama.

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

Penting untuk membuat konteks saat ini untuk operasi lebih lanjut dengan konteks (atau dengan sumber dan pendengar di dalamnya). Operasi akan kembali trueatau false, satu-satunya nilai kesalahan yang mungkin ditransmisikan alcGetErroradalah apa ALC_INVALID_CONTEXTyang jelas dari namanya.

Menyelesaikan dengan konteks, mis. ketika keluar dari program, perlu bahwa konteksnya tidak lagi saat ini, dan kemudian hancurkan.

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

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

Satu-satunya kesalahan yang mungkin dari alcDestroyContextadalah sama dengan alcMakeContextCurrent- ALC_INVALID_CONTEXT; jika Anda melakukan semuanya dengan benar, maka Anda tidak akan mendapatkannya, tetapi jika Anda melakukannya, maka tidak ada yang dapat dilakukan tentang hal itu.

Mengapa memeriksa kesalahan yang tidak dapat dilakukan oleh sesuatu?

Karena saya ingin pesan tentang mereka setidaknya muncul dalam aliran kesalahan, yang bagi kita tidak. alcCallMisalkan itu tidak pernah memberi kita kesalahan, tetapi akan berguna untuk mengetahui bahwa kesalahan seperti itu terjadi pada komputer orang lain. Berkat ini, kami dapat mempelajari masalahnya, dan mungkin melaporkan bug ke pengembang OpenAL Soft .

Mainkan suara pertama kami


Cukup semua ini, mari kita mainkan suaranya. Untuk memulainya, kita jelas membutuhkan file suara. Misalnya, yang ini, dari game yang pernah saya selesaikan .


Saya adalah pelindung sistem ini!

Jadi, buka IDE dan gunakan kode berikut. Ingatlah untuk memasukkan OpenAL Soft dan menambahkan kode unggah file dan kode pemeriksaan kesalahan yang ditunjukkan di atas.

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

Kompilasi! Kami menulis! Meluncurkan! Saya adalah pengawas sistem ini . Jika Anda tidak mendengar suara, periksa kembali semuanya. Jika ada sesuatu yang ditulis di jendela konsol, maka ini harus menjadi output standar dari aliran kesalahan, dan itu penting. Fungsi pelaporan kesalahan kami harus memberi tahu kami baris kode sumber yang menghasilkan kesalahan. Jika Anda menemukan

kesalahan, baca Panduan Programmer dan spesifikasi untuk memahami kondisi di mana kesalahan ini dapat dihasilkan oleh suatu fungsi. Ini akan membantu Anda mengetahuinya. Jika tidak berhasil, maka tinggalkan komentar di bawah artikel asli, dan saya akan mencoba membantu.

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

Ini mengacu pada kode boot gelombang. Yang penting adalah kami menerima data, baik sebagai penunjuk atau dikumpulkan dalam vektor: jumlah saluran, laju pengambilan sampel, dan jumlah bit per sampel.

Generasi penyangga


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

Mungkin terlihat akrab bagi Anda jika Anda pernah membuat buffer data tekstur di OpenGL. Intinya, kami membuat buffer dan berpura-pura hanya ada di kartu suara. Bahkan, kemungkinan besar akan disimpan dalam RAM biasa, tetapi spesifikasi OpenAL mengabstraksi semua operasi ini.

Jadi, nilainya ALuintadalah pegangan untuk buffer kita. Ingatlah bahwa buffer pada dasarnya adalah data suara dalam memori kartu suara. Kami tidak lagi memiliki akses langsung ke data ini, karena kami mengambilnya dari program (dari RAM biasa) dan memindahkannya ke kartu suara / chip, dll. OpenGL bekerja dengan cara yang sama, memindahkan data tekstur dari RAM ke VRAM. Deskriptor

inimenghasilkan alGenBuffers. Ini memiliki beberapa kemungkinan nilai kesalahan, yang paling penting adalah AL_OUT_OF_MEMORY, yang berarti bahwa kita tidak dapat lagi menambahkan data suara ke kartu suara. Anda tidak akan mendapatkan kesalahan ini jika, misalnya, Anda menggunakan buffer tunggal, tetapi Anda perlu mempertimbangkan ini jika Anda membuat mesin .

Tentukan format data 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;
}

Data suara berfungsi seperti ini: ada beberapa saluran dan ada ukuran bit per sampel . Data terdiri dari banyak sampel .

Untuk menentukan jumlah sampel dalam data audio, kami melakukan hal berikut:

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

Apa yang dapat dikonversi dengan mudah untuk menghitung durasi data audio:

std::size_t duration = numberOfSamples / sampleRate;

Tetapi sementara kita tidak perlu tahu juga numberOfSamples, juga durationpenting untuk mengetahui bagaimana semua informasi ini digunakan.

Kembali ke format- kita perlu memberi tahu OpenAL format data audio. Itu tampak jelas, bukan? Mirip dengan bagaimana kita mengisi buffer tekstur OpenGL, mengatakan bahwa data dalam urutan BGRA dan terdiri dari nilai 8-bit, kita perlu melakukan hal yang sama dalam OpenAL.

Untuk memberi tahu OpenAL bagaimana menafsirkan data yang ditunjuk oleh pointer yang akan kami lewati nanti, kita perlu mendefinisikan format data. Di bawah format , itu dimaksudkan sebagaimana dipahami oleh OpenAL. Hanya ada empat kemungkinan arti. Ada dua nilai yang mungkin untuk jumlah saluran: satu untuk mono, dua untuk stereo.

Selain jumlah saluran, kami memiliki jumlah bit per sampel. Itu sama dengan atau 8, atau 16, dan pada dasarnya kualitas suara.

Jadi dengan menggunakan nilai-nilai saluran dan bit per sampel, yang telah diinformasikan fungsi beban gelombang kepada kami, kami dapat menentukan mana yang akan ALenumdigunakan untuk parameter di masa mendatang format.

Mengisi buffer


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

Dengan ini, semuanya harus sederhana. Kami memuat ke dalam Penyangga OpenAL, yang ditunjuk oleh deskriptor buffer ; data ditunjukkan oleh ptr soundData.data()dalam ukuran sizeyang ditentukan sampleRate. Kami juga akan memberi tahu OpenAL format data ini melalui parameter format.

Pada akhirnya, kami cukup menghapus data yang diterima pemuat gelombang. Mengapa? Karena kami sudah menyalinnya ke kartu suara. Kita tidak perlu menyimpannya di dua tempat dan menghabiskan sumber daya berharga. Jika kartu suara kehilangan data, maka kami hanya akan mengunduhnya dari disk dan kami tidak perlu menyalinnya ke CPU atau orang lain.

Pengaturan sumber


Ingatlah bahwa OpenAL pada dasarnya adalah pendengar yang mendengarkan suara yang dibuat oleh satu atau lebih sumber . Nah, sekarang saatnya membuat sumber suara.

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

Sejujurnya, tidak perlu mengatur beberapa parameter ini, karena nilai defaultnya cukup cocok untuk kita. Tetapi ini menunjukkan kepada kami beberapa aspek yang dapat Anda coba dan lihat apa yang mereka lakukan (Anda bahkan dapat bertindak dengan cerdik dan mengubahnya dari waktu ke waktu).

Pertama kita menghasilkan sumber - ingat, ini lagi merupakan pegangan untuk sesuatu di dalam OpenAL API. Kami mengatur pitch (nada) agar tidak berubah, gain (volume) dibuat sama dengan nilai asli dari data audio, posisi dan kecepatan diatur ulang; kami tidak mengulang suara, karena jika tidak, program kami tidak akan pernah berakhir, dan menunjukkan buffer.

Ingat bahwa sumber yang berbeda dapat menggunakan buffer yang sama. Misalnya, musuh yang menembak pemain dari tempat yang berbeda dapat memainkan suara tembakan yang sama, jadi kami tidak memerlukan banyak salinan data suara, tetapi hanya beberapa tempat di ruang 3D dari mana suara itu dibuat.

Memainkan suara


alCall(alSourcePlay, source);

ALint state = AL_PLAYING;

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

Pertama, kita perlu mulai bermain sumber. Cukup telepon alSourcePlay.

Lalu kami membuat nilai untuk menyimpan kondisi AL_SOURCE_STATEsumber saat ini dan memperbaruinya tanpa henti. Ketika tidak lagi sama, AL_PLAYINGkita bisa melanjutkan. Anda dapat mengubah status AL_STOPPEDsaat selesai membuat suara dari buffer (atau ketika kesalahan terjadi). Jika Anda mengatur nilai untuk perulangan true, suara akan diputar selamanya.

Kemudian kita dapat mengubah buffer sumber dan memainkan suara lain. Atau memutar ulang suara yang sama, dll. Atur saja buffer, gunakan alSourcePlay, dan mungkin alSourceStop, jika perlu. Dalam artikel berikut kami akan mempertimbangkan ini secara lebih rinci.

Pembersihan


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

Karena kami hanya memutar data audio sekali dan keluar, kami akan menghapus sumber dan buffer yang dibuat sebelumnya.

Sisa kode dapat dimengerti tanpa penjelasan.

Ke mana harus pergi selanjutnya?


Mengetahui semua yang dijelaskan dalam artikel ini, Anda sudah dapat membuat game kecil! Cobalah untuk membuat Pong atau permainan klasik lainnya , bagi mereka lebih banyak tidak diperlukan.

Tapi ingat! Buffer ini hanya cocok untuk suara pendek, kemungkinan besar untuk beberapa detik. Jika Anda membutuhkan musik atau akting suara, Anda perlu mengalirkan audio ke OpenAL. Kami akan membicarakan ini di salah satu bagian berikut dari serangkaian tutorial.

All Articles