دليل للعمل مع OpenAL في C ++. الجزء 1: تشغيل الصوت

لعبتك تحتاج إلى صوت! ربما استخدمت بالفعل برنامج OpenGL للرسم على الشاشة. لقد اكتشفت واجهة برمجة التطبيقات الخاصة به ، ولذلك لجأت إلى OpenAL لأن الاسم يبدو مألوفًا.

حسنًا ، الخبر السار هو أن OpenAL لديها أيضًا واجهة برمجة تطبيقات مألوفة جدًا. تم تصميمه في الأصل لمحاكاة واجهة برمجة تطبيقات مواصفات OpenGL. لذلك اخترت ذلك من بين العديد من أنظمة الصوت للألعاب. بالإضافة إلى ذلك ، هو عبر منصة.

في هذه المقالة ، سأتحدث بالتفصيل عن الرمز المطلوب لاستخدام OpenAL في لعبة مكتوبة بلغة C ++. سنناقش الأصوات والموسيقى ووضع الصوت في الفضاء ثلاثي الأبعاد مع أمثلة التعليمات البرمجية.

تاريخ OpenAL


سأحاول الإيجاز. كما ذكر أعلاه ، فقد تم تصميمه عن عمد كتقليد لـ OpenGL API ، وهناك سبب لذلك. هذه واجهة برمجة تطبيقات مريحة ومعروفة للكثيرين ، وإذا كانت الرسومات جانبًا واحدًا من محرك اللعبة ، فيجب أن يكون الصوت مختلفًا. في البداية ، كان من المفترض أن يكون OpenAL مفتوح المصدر ، ولكن بعد ذلك حدث شيء ...

الناس ليسوا مهتمين بالصوت كرسومات ، لذا قام Creative في نهاية المطاف بجعل OpenAL ملكًا له ، وأصبح تنفيذ المرجع الآن ملكية وليس مجانيًا. لكن! لا تزال مواصفات OpenAL معيارًا "مفتوحًا" ، أي أنه تم نشره .

من وقت لآخر ، يتم تعديل المواصفات ، ولكن ليس كثيرًا. لا يتغير الصوت بسرعة الرسومات ، لأنه لا توجد حاجة خاصة لذلك.

سمحت المواصفات المفتوحة لأشخاص آخرين بإنشاء تطبيق مفتوح المصدر للمواصفات. أحد هذه التطبيقات هو OpenAL Soft ، وبصراحة ، لا معنى للبحث عن أي آخرين. هذا هو التطبيق الذي سأستخدمه ، وأوصي باستخدامه أيضًا.

هي عبر منصة. يتم تنفيذه بشكل غريب - في الواقع ، داخل المكتبة تستخدم واجهات برمجة تطبيقات صوتية أخرى موجودة في نظامك. في Windows ، يستخدم DirectSound ، على Unix ، OSS . بفضل هذا ، كانت قادرة على أن تصبح عبر منصة. في جوهرها ، هذا اسم كبير لواجهة برمجة التطبيقات المجمعة.

قد تكون قلقًا بشأن سرعة واجهة برمجة التطبيقات هذه. لكن لا تقلق. هذا هو نفس الصوت ، ولا يخلق حمولة كبيرة ، لذلك لا يتطلب التحسينات الكبيرة التي تتطلبها واجهة برمجة التطبيقات للرسومات.

لكن قصة كافية ، دعنا ننتقل إلى التكنولوجيا.

ماذا تحتاج لكتابة كود في OpenAL؟


تحتاج إلى بناء OpenAL Soft في سلسلة الأدوات التي تختارها. هذه عملية بسيطة جدًا يمكنك اتباعها وفقًا للإرشادات الواردة في قسم تثبيت المصدر . لم أواجه أي مشاكل في هذا الأمر أبدًا ، ولكن إذا واجهتك أي صعوبات ، فاكتب تعليقًا أسفل المقالة الأصلية أو اكتب إلى قائمة بريد OpenAL Soft .

بعد ذلك ، ستحتاج إلى بعض الملفات الصوتية وطريقة لتنزيلها. تحميل بيانات الصوت في المخازن المؤقتة والتفاصيل الدقيقة لتنسيقات الصوت المختلفة خارج نطاق هذه المقالة ، ولكن يمكنك القراءة حول تنزيل وتدفق ملفات Ogg / Vorbis . تنزيل ملفات WAV بسيط للغاية ، هناك بالفعل مئات المقالات على الإنترنت حول هذا الموضوع.

مهمة البحث عن الملفات الصوتية التي عليك أن تقررها بنفسك. هناك العديد من الضوضاء والانفجارات على الإنترنت يمكنك تنزيلها. إذا كانت لديك شائعة ، فيمكنك محاولة كتابة موسيقى شيبتون الخاصة بك [ الترجمة على حبري].

أيضًا ، احتفظ بدليل المبرمجين من OpenALSoft في متناول يدك . هذه الوثائق أفضل بكثير مع التخصص "الرسمي".

هذا ، في الواقع ، كل شيء. سنفترض أنك تعرف بالفعل كيفية كتابة التعليمات البرمجية ، واستخدام IDE وسلسلة الأدوات.

نظرة عامة حول OpenAL API


كما قلت عدة مرات ، إنها مشابهة لـ OpenGL API. يكمن التشابه في حقيقة أنه يقوم على الحالات وتتفاعل مع الواصفات / المعرّفات ، وليس مع الأشياء نفسها مباشرة.

هناك اختلافات بين اصطلاحات API في OpenGL و OpenAL ، لكنها ليست كبيرة. في OpenGL ، تحتاج إلى إجراء مكالمات نظام تشغيل خاصة لإنشاء سياق عرض. تختلف هذه التحديات باختلاف أنظمة التشغيل وليست حقًا جزءًا من مواصفات OpenGL. في OpenAL ، كل شيء مختلف - وظائف إنشاء السياق هي جزء من المواصفات وهي نفسها بغض النظر عن نظام التشغيل.

عند التفاعل مع API ، هناك ثلاثة أنواع رئيسية من الكائنات التي تتفاعل معها. المستمعين("المستمعون") هو موقع "الأذنين" الموجود في مساحة ثلاثية الأبعاد (يوجد دائمًا مستمع واحد فقط). المصادر ("المصادر") هي "مكبرات صوت" تصدر صوتًا ، مرة أخرى في الفضاء ثلاثي الأبعاد. يمكن نقل المستمع والمصادر في الفضاء ، وهذا يتوقف على ما تسمعه من خلال مكبرات الصوت في اللعبة يتغير.

الكائنات الأخيرة هي مخازن . يقومون بتخزين عينات من الأصوات التي ستلعبها المصادر للمستمعين.

هناك أيضًا أوضاع تستخدمها اللعبة لتغيير طريقة معالجة الصوت من خلال OpenAL.

المصادر


كما ذكر أعلاه ، هذه الأشياء هي مصادر الأصوات. يمكن تعيين موضعها واتجاهها ، وهي مرتبطة بذاكرة تخزين مؤقت للبيانات الصوتية للتشغيل.

مستمع


المجموعة الوحيدة من "الأذنين" في اللعبة. يتم إعادة إنتاج ما يسمعه المستمع من خلال مكبرات الصوت في الكمبيوتر. لديه أيضا منصب.

مخازن


في OpenGL ، ما يعادلها هو Texture2D. في جوهرها ، هذه هي البيانات الصوتية التي يستنسخها المصدر.

أنواع البيانات


لتكون قادرة على دعم التعليمات البرمجية عبر الأنظمة الأساسية ، تقوم OpenAL بتنفيذ سلسلة معينة من الإجراءات وتحدد بعض أنواع البيانات. في الواقع ، إنه يتبع برنامج OpenGL بدقة بحيث يمكننا تحويل أنواع OpenAL مباشرة إلى أنواع OpenGL. يسرد الجدول أدناه لهم وما يعادلها.

اكتب openalاكتب openalcاكتب openglC ++ Typedefوصف
ALbooleanALCbooleanGLbooleanstd::int8_tقيمة منطقية 8 بت
ALbyteALCbyteGLbytestd::int8_tقيمة عددية 8 بت للرمز الإضافي بعلامة
ALubyteALCubyteGLubytestd::uint8_tقيمة عددية غير موقعة 8 بت
ALcharALCcharGLcharcharرمز
ALshortALCshortGLshortstd::int16_tقيمة عددية 16 بت موقعة
ALushortALCushortGLushortstd::uint16_t16-
ALintALCintGLintstd::int32_t32-
ALuintALCuintGLuintstd::uint32_t32-
ALsizeiALCsizeiGLsizeistd::int32_t32-
ALenumALCenumGLenumstd::uint32_t32-
ALfloatALCfloatGLfloatfloat32- IEEE 754
ALdoubleALCdoubleGLdoubledouble64- IEEE 754
ALvoidALCvoidGLvoidvoid

OpenAL


هناك مقال حول كيفية تبسيط التعرف على أخطاء OpenAL ، ولكن من أجل الاكتمال ، سأكررها هنا. هناك نوعان من مكالمات OpenAL API: عادية وسياقية. تشبه

مكالمات السياق التي تبدأ alcبـ OpenGL win32 المكالمات للحصول على سياق العرض أو نظرائهم على Linux. الصوت هو شيء بسيط بما يكفي لجميع أنظمة التشغيل للحصول على نفس المكالمات. تبدأ المكالمات العادية بـ al. للحصول على أخطاء في المكالمات السياقية ، نسميها alcGetError؛ في حالة المكالمات المنتظمة ، نسميها alGetError. ترجع إما قيمة ALCenumأو قيمة ALenumتسرد الأخطاء المحتملة.

الآن سننظر في حالة واحدة فقط ، ولكن في كل شيء آخر هم تقريبًا نفس الشيء. لنأخذ التحديات المعتادة al. أولاً ، قم بإنشاء ماكرو معالج مسبق للقيام بالمهمة المملة لتمرير التفاصيل:

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

من الناحية النظرية ، قد لا يدعم المترجم الخاص بك __FILE__أيضًا __LINE__، ولكن ، لأكون صادقًا ، سأفاجأ إذا تبين أن ذلك صحيح. __VA_ARGS__يشير إلى عدد متغير من الوسيطات التي يمكن تمريرها إلى هذا الماكرو.

بعد ذلك ، نقوم بتنفيذ وظيفة تتلقى يدويًا آخر خطأ تم الإبلاغ عنه وتعرض قيمة واضحة لدفق الخطأ القياسي.

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

ليس هناك الكثير من الأخطاء المحتملة. التفسيرات التي كتبتها في الكود هي المعلومات الوحيدة التي ستتلقاها حول هذه الأخطاء ، لكن المواصفات توضح سبب قيام دالة معينة بإرجاع خطأ معين.

ثم ننفذ وظيفتين مختلفتين للقالب ستلف جميع مكالمات 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);
}

هناك نوعان منها ، لأن الأولى تُستخدم لوظائف OpenAL التي تُرجع void، والثانية تُستخدم عندما تُرجع الدالة قيمة غير فارغة. إذا لم تكن على دراية كبيرة بقوالب البرمجة التخطيطية في C ++ ، فألق نظرة على أجزاء الرمز c std::enable_if. إنها تحدد أي من وظائف القالب هذه يتم تنفيذها من قبل المترجم لكل استدعاء دالة.

والآن نفس الشيء للمكالمات 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);
}

التغيير الأكبر هو التضمين device، الذي تستخدمه جميع المكالمات alc، بالإضافة إلى الاستخدام المقابل لأخطاء النمط ALCenumو ALC_. إنها تبدو متشابهة جدًا ، ولمدة طويلة جدًا ، تغيرت التغييرات الصغيرة من التعليمات البرمجية وفهمي alإلى alcالضرر الشديد ، لذلك واصلت القراءة مباشرةً فوقها c.

هذا كل شئ. عادةً ما تبدو مكالمة OpenAL في C ++ كأحد الخيارات التالية:

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

ولكن الآن يمكننا القيام بذلك على النحو التالي:

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

قد يبدو الأمر غريبًا بالنسبة لك ، ولكنه أكثر ملاءمة بالنسبة لي. بالطبع ، يمكنك اختيار هيكل مختلف.

تنزيل ملفات wav


يمكنك تنزيلها بنفسك أو استخدام المكتبة. فيما يلي تطبيق مفتوح المصدر لتحميل ملفات wav . أنا مجنونة ، لذلك أقوم بذلك بنفسي:

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

لن أشرح الشفرة ، لأن هذا ليس بالكامل في موضوع مقالنا ؛ ولكن من الواضح جدًا إذا قرأته بالتوازي مع مواصفات ملف WAV .

التهيئة والتدمير


نحتاج أولاً إلى تهيئة OpenAL ، ثم ، مثل أي مبرمج جيد ، ننهيها عندما ننتهي من العمل معها. يتم استخدامه أثناء التهيئة ALCdevice(لاحظ أن هذا ALCهو لا AL )، الذي يمثل أساسا شيء على جهاز الكمبيوتر الخاص بك لتشغيل الموسيقى الخلفية والاستخدامات عليه ALCcontext.

ALCdeviceعلى غرار اختيار بطاقة رسومات. الذي سيتم عرض لعبة OpenGL عليه. ALCcontextعلى غرار سياق العرض الذي تريد إنشاءه (فريد لنظام التشغيل) لـ OpenGL.

Alcdevice


جهاز OpenAL هو ما يتم من خلاله إخراج الصوت ، سواء كان بطاقة صوت أو شريحة ، ولكن من الناحية النظرية يمكن أن يكون هناك الكثير من الأشياء المختلفة. على غرار كيف iostreamيمكن أن يكون الإخراج القياسي طابعة بدلاً من شاشة ، يمكن أن يكون الجهاز ملفًا أو حتى دفق بيانات.

ومع ذلك ، بالنسبة لألعاب البرمجة ، سيكون جهاز صوت ، وعادة ما نريده أن يكون جهاز إخراج صوت قياسي في النظام.

للحصول على قائمة بالأجهزة المتاحة في النظام ، يمكنك طلبها بهذه الوظيفة:

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

هذا في الواقع مجرد غلاف حول غلاف حول مكالمة alcGetString. القيمة المرجعة هي مؤشر لقائمة من السلاسل المفصولة بقيمة nullوتنتهي بقيمتين null. هنا ، يقوم الغلاف ببساطة بتحويله إلى ناقل مناسب لنا.

لحسن الحظ ، لسنا بحاجة للقيام بذلك! في الحالة العامة ، كما أعتقد ، يمكن لمعظم الألعاب ببساطة إخراج الصوت للجهاز بشكل افتراضي ، مهما كان. نادرًا ما أرى خيارات تغيير جهاز الصوت الذي تريد من خلاله إخراج الصوت. لذلك ، لتهيئة جهاز OpenAL ، نستخدم مكالمة alcOpenDevice. تختلف هذه المكالمة قليلاً عن أي شيء آخر ، لأنها لا تحدد حالة الخطأ التي يمكن الحصول عليها من خلالها alcGetError، لذلك نسميها مثل وظيفة عادية:

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

إذا قمت بإدراج الأجهزة كما هو موضح أعلاه ، وتريد من المستخدم تحديد أحدها ، فأنت بحاجة إلى نقل اسمه إلى alcOpenDeviceبدلاً من ذلك nullptr. إرسال nullptrأوامر لفتح الجهاز بشكل افتراضي . القيمة المرتجعة هي إما الجهاز المقابل ، أو في nullptrحالة حدوث خطأ.

اعتمادًا على ما إذا كنت قد أكملت التعداد أم لا ، فقد يؤدي خطأ إلى إيقاف البرنامج على المسارات. لا يوجد جهاز = لا OpenAL ؛ لا OpenAL = لا يوجد صوت ؛ لا صوت = لا لعبة.

آخر شيء نقوم به عند إغلاق البرنامج هو إكماله بشكل صحيح.

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

في هذه المرحلة ، إذا كان الإنجاز غير ممكن ، فهذا لم يعد مهمًا بالنسبة لنا. قبل إغلاق الجهاز ، يجب علينا إغلاق جميع السياقات التي تم إنشاؤها ، ومع ذلك ، في تجربتي ، هذه المكالمة تكمل أيضًا السياق. لكننا سنفعل ذلك بشكل صحيح. إذا أكملت كل شيء قبل إجراء مكالمة alcCloseDevice، فلا يجب أن تكون هناك أخطاء ، وإذا نشأت لسبب ما ، فلا يمكنك فعل أي شيء حيال ذلك.

ربما لاحظت أن المكالمات الواردة من alcCallإرسال نسختين من الجهاز. حدث ذلك بسبب كيفية عمل وظيفة القالب - هناك حاجة إلى واحدة للتحقق من الأخطاء ، والثانية تستخدم كمعلمة دالة.

من الناحية النظرية ، يمكنني تحسين وظيفة القالب بحيث يمر المعلمة الأولى للتحقق من الأخطاء وما زال يرسلها إلى الوظيفة ؛ لكنني كسول للقيام بذلك. سأترك هذا واجبك.

ALCcontext لدينا


الجزء الثاني من التهيئة هو السياق. كما كان من قبل ، يشبه سياق العرض من OpenGL. يمكن أن يكون هناك العديد من السياقات في برنامج واحد ويمكننا التبديل بينها ، ولكننا لن نحتاج إلى ذلك. كل سياق لديها قناعاتها المستمع و المصادر ، وأنها لا يمكن أن تنتقل بين السياقات.

ربما يكون هذا مفيدًا في برنامج معالجة الصوت. ومع ذلك ، بالنسبة للألعاب في 99.9٪ من الحالات ، يكفي سياق واحد فقط.

إنشاء سياق جديد أمر بسيط للغاية:

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

نحن بحاجة إلى التواصل من أجل ما ALCdeviceنريد إنشاء سياق ؛ يمكننا أيضًا تمرير قائمة اختيارية ذات قيمة صفرية من المفاتيح والقيم ALCint، وهي سمات يجب إنشاء السياق من خلالها.

بصراحة ، أنا لا أعرف حتى في أي حالة يكون تمرير سمة مفيدًا. سيتم تشغيل اللعبة على جهاز كمبيوتر عادي مع ميزات الصوت المعتادة. للسمات قيم افتراضية ، اعتمادًا على جهاز الكمبيوتر ، لذلك هذا ليس مهمًا بشكل خاص. ولكن في حال كنت لا تزال بحاجة إليها:

اسم السمةوصف
ALC_FREQUENCYتردد الخلط إلى المخزن المؤقت للإخراج ، يقاس بالهرتز
ALC_REFRESHفترات التحديث ، تقاس هرتز
ALC_SYNC0أو 1تشير إلى ما إذا كان يجب أن يكون سياق متزامن أو غير متزامن
ALC_MONO_SOURCESقيمة تساعدك في إخبارك بعدد المصادر التي ستستخدمها والتي تتطلب القدرة على معالجة البيانات الصوتية أحادية الصوت. لا يحد من الحد الأقصى للمبلغ ، فهو يسمح لك فقط بأن تكون أكثر فعالية عندما تعرف ذلك مسبقًا.
ALC_STEREO_SOURCESنفس الشيء ، ولكن بالنسبة لبيانات الاستريو.

إذا حصلت على أخطاء ، فمن المرجح أن هذا يرجع إلى أن السمات التي تريدها مستحيلة أو لا يمكنك إنشاء سياق آخر للجهاز المدعوم ؛ سيؤدي هذا إلى حدوث خطأ ALC_INVALID_VALUE. إذا قمت بتمرير جهاز غير صالح ، فسوف تحصل على خطأ ALC_INVALID_DEVICE، ولكننا بالطبع نتحقق من هذا الخطأ.

إنشاء السياق لا يكفي. ما زلنا بحاجة إلى جعله محدثًا - يبدو مثل سياق عرض Windows OpenGL ، أليس كذلك؟ نفس الشيء.

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

من الضروري جعل السياق الحالي لأي عمليات أخرى مع السياق (أو مع المصادر والمستمعين فيه). ستعود العملية trueأو false، قيمة الخطأ المحتملة الوحيدة alcGetErrorالتي تم إرسالها هي تلك ALC_INVALID_CONTEXTالتي تكون واضحة من الاسم.

الانتهاء مع السياق ، أي عند الخروج من البرنامج ، من الضروري ألا يكون السياق الحالي ، ومن ثم تدميره.

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

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

الخطأ ممكنا إلا من alcDestroyContextهو نفسه كما ان من alcMakeContextCurrent- ALC_INVALID_CONTEXT. إذا فعلت كل شيء بشكل صحيح ، فلن تحصل عليه ، ولكن إذا فعلت ذلك ، فلا يمكن فعل شيء حيال ذلك.

لماذا التحقق من الأخطاء التي لا يمكن فعل شيء معها؟

لأنني أريد أن تظهر الرسائل المتعلقة بها على الأقل في تدفق الأخطاء ، وهو ما يحدث لنا. alcCallلنفترض أنه لا يعطينا أخطاء أبدًا ، ولكن سيكون من المفيد معرفة أن مثل هذا الخطأ يحدث على كمبيوتر شخص آخر. بفضل هذا ، يمكننا دراسة المشكلة ، وربما الإبلاغ عن خطأ لمطوري OpenAL Soft .

تشغيل صوتنا الأول


حسنًا ، يكفي كل هذا ، فلنقم بتشغيل الصوت. بادئ ذي بدء ، من الواضح أننا بحاجة إلى ملف صوتي. على سبيل المثال ، هذه اللعبة من لعبة سوف أنتهي من أي وقت مضى .


أنا حامي هذا النظام!

لذا ، افتح IDE واستخدم الكود التالي. تذكر أن تقوم بتوصيل OpenAL Soft وإضافة رمز تحميل الملف ورمز التحقق من الأخطاء الموضح أعلاه.

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

تجميع! نحن نؤلف! إطلاق! أنا مقدِّم هذا النظام . إذا لم تسمع الصوت ، فقم بفحص كل شيء مرة أخرى. إذا تم كتابة شيء ما في نافذة وحدة التحكم ، فيجب أن يكون هذا هو الناتج القياسي لدفق الخطأ ، وهو أمر مهم. يجب أن تخبرنا وظائف الإبلاغ عن الأخطاء لدينا بسطر شفرة المصدر الذي أدى إلى الخطأ. إذا وجدت

خطأ، وقراءة دليل المبرمجين و مواصفات لفهم الظروف التي يمكن أن تتولد هذا الخطأ عن طريق وظيفة. هذا سيساعدك على معرفة ذلك. إذا لم تنجح ، فاترك تعليقًا تحت المقالة الأصلية ، وسأحاول المساعدة.

قم بتنزيل بيانات 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;
}

يشير هذا إلى رمز تمهيد الموجة. الشيء المهم هو أننا نتلقى بيانات ، إما كمؤشر ، أو مجمعة في ناقل: عدد القنوات ، ومعدل أخذ العينات وعدد البتات لكل عينة.

الجيل العازل


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

ربما يبدو مألوفًا لك إذا قمت بإنشاء مخازن بيانات نسيج في OpenGL. في الأساس ، نقوم بإنشاء مخزن مؤقت ونتظاهر بأنه سيكون موجودًا فقط في بطاقة الصوت. في الواقع ، سيتم تخزينه على الأرجح في ذاكرة الوصول العشوائي العادية ، ولكن مواصفات OpenAL تلخص كل هذه العمليات.

لذا ، فإن القيمة ALuintهي مقبض المخزن المؤقت لدينا. تذكر أن المخزن المؤقت هو بيانات سليمة في ذاكرة بطاقة الصوت. لم يعد لدينا وصول مباشر إلى هذه البيانات ، حيث أخذناها من البرنامج (من ذاكرة الوصول العشوائي العادية) ونقلناها إلى بطاقة الصوت / الشريحة ، وما إلى ذلك. يعمل OpenGL بالمثل ، حيث ينقل بيانات النسيج من ذاكرة الوصول العشوائي إلى VRAM.

هذا الواصفيولد alGenBuffers. يحتوي على اثنين من قيم الخطأ المحتملة ، وأهمها AL_OUT_OF_MEMORY، مما يعني أنه لم يعد بإمكاننا إضافة بيانات الصوت إلى بطاقة الصوت. لن تحصل على هذا الخطأ إذا كنت ، على سبيل المثال ، تستخدم مخزنًا مؤقتًا واحدًا ، ولكنك تحتاج إلى التفكير في ذلك إذا كنت تقوم بإنشاء محرك .

تحديد تنسيق البيانات الصوتية


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

تعمل البيانات الصوتية على النحو التالي: هناك العديد من القنوات وهناك حجم صغير لكل عينة . تتكون البيانات من العديد من العينات .

لتحديد عدد العينات في البيانات الصوتية ، نقوم بما يلي:

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

ما يمكن تحويله بسهولة لحساب مدة البيانات الصوتية:

std::size_t duration = numberOfSamples / sampleRate;

ولكن في حين أننا لسنا بحاجة إلى معرفة لا numberOfSamples، ولا duration، مع ذلك ، من المهم معرفة كيفية استخدام كل هذه المعلومات.

عودة إلى format- نحتاج إلى إخبار OpenAL بتنسيق بيانات الصوت. يبدو ذلك واضحًا ، أليس كذلك؟ على غرار كيفية ملء المخزن المؤقت لمادة OpenGL ، قائلة إن البيانات في تسلسل BGRA وتتكون من قيم 8 بت ، نحتاج إلى القيام بنفس الشيء في OpenAL.

لإخبار OpenAL بكيفية تفسير البيانات التي يشير إليها المؤشر الذي سنمرره لاحقًا ، نحتاج إلى تعريف تنسيق البيانات. تحت التنسيق ، يقصد به OpenAL. لا يوجد سوى أربعة معاني محتملة. هناك قيمتان محتملتان لعدد القنوات: واحدة للأحادية ، واثنتان للاستريو.

بالإضافة إلى عدد القنوات ، لدينا عدد البتات لكل عينة. إنه يساوي أو 8، أو 16، وهو جودة الصوت بشكل أساسي.

لذا ، باستخدام قيم القنوات والبتات لكل عينة ، والتي أبلغتنا بها وظيفة تحميل الموجة ، يمكننا تحديد أيها ALenumنستخدمه للمعلمة المستقبلية format.

تعبئة عازلة


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

مع هذا ، يجب أن يكون كل شيء بسيطًا. نقوم بالتحميل في مخزن OpenAL Buffer ، الذي يشير إليه الواصف buffer ؛ البيانات المشار إليها بواسطة ptr soundData.data()في حجم sizeالمحدد sampleRate. سنقوم أيضًا بإبلاغ OpenAL بتنسيق هذه البيانات من خلال المعلمة format.

في النهاية ، نقوم ببساطة بحذف البيانات التي تلقاها محمل الموجة. لماذا ا؟ لأننا قمنا بنسخها بالفعل إلى بطاقة الصوت. لسنا بحاجة لتخزينها في مكانين وإنفاق موارد ثمينة. إذا فقدت بطاقة الصوت البيانات ، فسنقوم ببساطة بتنزيلها من القرص مرة أخرى ولن نحتاج إلى نسخها إلى وحدة المعالجة المركزية أو أي شخص آخر.

إعداد المصدر


يذكر أن مكتبة الصوت المفتوحة هو في الأساس المستمع أن يستمع إلى الأصوات التي أدلى بها واحد أو أكثر من مصادر . حسنًا ، حان الوقت الآن لإنشاء مصدر صوت.

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

بصراحة ، ليس من الضروري تعيين بعض هذه المعلمات ، لأن القيم الافتراضية مناسبة تمامًا لنا. لكن هذا يوضح لنا بعض الجوانب التي يمكنك من خلالها تجربة ومعرفة ما يفعلونه (يمكنك حتى التصرف بمكر وتغييرها بمرور الوقت).

أولاً ننشئ المصدر - تذكر ، هذا مرة أخرى مقبض لشيء داخل OpenAL API. نضع درجة (نغمة) بحيث لا تتغير ، ويتم كسب (حجم) مساوٍ للقيمة الأصلية لبيانات الصوت ، ويتم إعادة تعيين الموقع والسرعة ؛ نحن لا نكرر الصوت ، وإلا فلن ينتهي برنامجنا أبدًا ، ويشير إلى المخزن المؤقت.

تذكر أن المصادر المختلفة يمكنها استخدام نفس المخزن المؤقت. على سبيل المثال ، يمكن للأعداء الذين يطلقون النار على لاعب من أماكن مختلفة تشغيل نفس صوت اللقطة ، لذلك لا نحتاج إلى العديد من نسخ بيانات الصوت ، ولكن فقط عدد قليل من الأماكن في الفضاء ثلاثي الأبعاد الذي يصدر منه الصوت.

تشغيل الصوت


alCall(alSourcePlay, source);

ALint state = AL_PLAYING;

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

نحتاج أولاً إلى بدء تشغيل المصدر. ببساطة اتصل alSourcePlay.

ثم نقوم بإنشاء قيمة لتخزين الحالة الحالية AL_SOURCE_STATEللمصدر وتحديثها إلى ما لا نهاية. عندما لا تكون متساوية ، AL_PLAYINGيمكننا الاستمرار. يمكنك تغيير الحالة إلى AL_STOPPEDعندما تنتهي من إصدار الصوت من المخزن المؤقت (أو عند حدوث خطأ). إذا قمت بتعيين قيمة التكرار true، فسيتم تشغيل الصوت إلى الأبد.

ثم يمكننا تغيير المخزن المؤقت المصدر وتشغيل صوت آخر. أو إعادة تشغيل نفس الصوت ، إلخ. ما عليك سوى تعيين المخزن المؤقت واستخدامه alSourcePlayوربما alSourceStopإذا لزم الأمر. في المقالات التالية سننظر في هذا بمزيد من التفصيل.

تنظيف


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

نظرًا لأننا نقوم ببساطة بتشغيل البيانات الصوتية مرة واحدة ونخرج ، فسنحذف المصدر والمخزن المؤقت اللذين تم إنشاؤهما مسبقًا.

بقية الشفرة مفهومة بدون تفسير.

إلى أين أذهب بعد ذلك؟


بمعرفة كل شيء موصوف في هذه المقالة ، يمكنك بالفعل إنشاء لعبة صغيرة! حاول إنشاء لعبة Pong أو أي لعبة كلاسيكية أخرى ، لأن المزيد منها غير مطلوب.

لكن تذكر! هذه المخازن المؤقتة مناسبة فقط للأصوات القصيرة ، على الأرجح لبضع ثوان. إذا كنت بحاجة إلى الموسيقى أو التمثيل الصوتي ، فستحتاج إلى دفق الصوت إلى OpenAL. سنتحدث عن هذا في أحد الأجزاء التالية من سلسلة من البرامج التعليمية.

All Articles