تطوير محتوى صوتي للعبة باستخدام Web Audio API

مقدمة

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

الألعاب ليست استثناء. أجمل ذكرياتي في ألعاب الفيديو هي الموسيقى والمؤثرات الصوتية. وفي الكثير من الحالات وبعد مرور عقدَين تقريبًا على تشغيل الأغاني المفضّلة لديّ، لم أتمكّن من الحصول على تركيبات Zelda للفنان كوجي كوندو وموسيقى ديابلو الصوتية الرائعة التي أنشأها مات أولمين. تنطبق درجة الجاذبية نفسها على التأثيرات الصوتية، مثل استجابات النقر على الوحدة التي يمكن التعرف عليها فورًا من Warcraft، والعينات من كلاسيكيات Nintendo.

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

محتوى صوتي للألعاب على الويب

بالنسبة إلى الألعاب البسيطة، قد يكفي استخدام العلامة <audio>. مع ذلك، تستخدم العديد من المتصفحات عمليات تنفيذ سيئة، ما يؤدي إلى حدوث خلل في الصوت وارتفاع وقت الاستجابة. نأمل أن تكون هذه مشكلة مؤقتة، نظرًا لأن البائعين يعملون بجد لتحسين عمليات التنفيذ الخاصة بهم. للحصول على لمحة عن حالة علامة <audio>، يمكنك الاطّلاع على مجموعة اختبار رائعة على areweplayingyet.org.

ومع ذلك، بالنظر إلى مواصفات علامة <audio>، اتّضح أنّ هناك العديد من الإجراءات التي يتعذّر تنفيذها بها، وهذا أمر غير مفاجئ، لأنّها مصمّمة لتشغيل الوسائط. تتضمن بعض القيود ما يلي:

  • لا يمكن تطبيق الفلاتر على الإشارة الصوتية.
  • لا توجد طريقة للوصول إلى بيانات PCM الأولية
  • ما مِن مفهوم لموقف المصادر والمستمعين واتجاهها.
  • ما مِن توقيت دقيق.

في بقية المقالة، سأتعمق في بعض هذه الموضوعات في سياق صوت اللعبة المكتوب باستخدام Web Audio API. للحصول على مقدمة موجزة عن واجهة برمجة التطبيقات هذه، يمكنك الاطّلاع على البرنامج التعليمي للبدء.

موسيقى الخلفية

غالبًا ما يتم تشغيل موسيقى في الخلفية بشكل متكرّر.

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

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

سوار المرآب

بعد ذلك، باستخدام Web Audio API، يمكنك استيراد كل هذه النماذج باستخدام فئة معيّنة مثل فئة BufferLoader عبر XHR (تم تناول هذا بالتفصيل في المقالة التمهيدية حول Web Audio API. يستغرق تحميل المحتوى بعض الوقت، لذا يجب تحميل مواد العرض المستخدَمة في اللعبة عند تحميل الصفحة أو في بداية المستوى أو ربما بشكل تدريجي أثناء اللعب.

بعد ذلك، يمكنك إنشاء مصدر لكل عقدة، وعقدة ربح لكل مصدر، وربط الرسم البياني.

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

// Assume gains is an array of AudioGainNode, normVal is the intensity
// between 0 and 1.
var value = normVal - (gains.length - 1);
// First reset gains on all nodes.
for (var i = 0; i < gains.length; i++) {
    gains[i].gain.value = 0;
}
// Decide which two nodes we are currently between, and do an equal
// power crossfade between them.
var leftNode = Math.floor(value);
// Normalize the value between 0 and 1.
var x = value - leftNode;
var gain1 = Math.cos(x - 0.5*Math.PI);
var gain2 = Math.cos((1.0 - x) - 0.5*Math.PI);
// Set the two gains accordingly.
gains[leftNode].gain.value = gain1;
// Check to make sure that there's a right node.
if (leftNode < gains.length - 1) {
    // If there is, adjust its gain.
    gains[leftNode + 1].gain.value = gain2;
}

في النهج السابق، يتم تشغيل مصدرين في آن واحد، ويتم عرضهما بطريقة تقاطع بينهما باستخدام منحنيات القوة المتساوية (كما هو موضّح في المقدمة).

يستخدم العديد من مطوّري الألعاب اليوم علامة <audio> للموسيقى في الخلفية لأنّها مناسبة تمامًا لبث المحتوى. يمكنك الآن جلب المحتوى من العلامة <audio> إلى سياق Web Audio.

قد تكون هذه التقنية مفيدة لأنّ العلامة <audio> يمكن أن تعمل مع محتوى البث، ما يتيح لك تشغيل موسيقى الخلفية فورًا بدلاً من الانتظار إلى أن يتم تنزيل المحتوى. من خلال إضافة البث إلى Web Audio API، يمكنك معالجة البث أو تحليله. يطبّق المثال التالي فلتر التمرير المنخفض على الموسيقى التي تمّ تشغيلها من خلال علامة <audio>:

var audioElement = document.querySelector('audio');
var mediaSourceNode = context.createMediaElementSource(audioElement);
// Create the filter
var filter = context.createBiquadFilter();
// Create the audio graph.
mediaSourceNode.connect(filter);
filter.connect(context.destination);

للحصول على مناقشة أكثر اكتمالاً حول دمج علامة <audio> مع Web Audio API، يُرجى الاطّلاع على هذه المقالة القصيرة.

المؤثرات الصوتية

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

من الميزات الرئيسية الأخرى للتأثيرات الصوتية في الألعاب إمكانية توفُّر العديد منها في آن واحد. تخيل أنك في معركة مسلحة مع عدة ممثلين تطلق البنادق الآلية. تطلق كل بنادق آلية عدة مرات في الثانية، مما يؤدي إلى تشغيل عشرات المؤثرات الصوتية في الوقت نفسه. إن تشغيل صوت من مصادر متعددة محددة بالوقت بدقة في الوقت نفسه هو أحد الأماكن التي تتألق فيها واجهة برمجة تطبيقات Web Audio.

في المثال التالي، يتم إنشاء جولة مدفع آلة من عينات رصاصة فردية متعددة من خلال إنشاء مصادر صوت متعددة يتم تشغيلها بترتيب زمني.

var time = context.currentTime;
for (var i = 0; i < rounds; i++) {
    var source = this.makeSource(this.buffers[M4A1]);
    source.noteOn(time + i - interval);
}

الآن، إذا كانت جميع البنادق الآلية في لعبتك تبدو هكذا بالضبط، فسيكون ذلك مملاً جدًا. ستختلف بالطبع حسب الصوت بناءً على المسافة من الهدف والموضع النسبي (المزيد عن هذا الموضوع لاحقًا)، لكن قد لا يكون ذلك كافيًا أيضًا. لحسن الحظ، توفر Web Audio API طريقة لتعديل المثال أعلاه بسهولة بطريقتين:

  1. مع تغيُّر بسيط في الوقت بين إطلاق النار
  2. من خلال تغيير معدل التشغيل لكل عينة (مع تغيير درجة الصوت أيضًا) لمحاكاة العشوائية في العالم الحقيقي بشكل أفضل

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

صوت موضعي ثلاثي الأبعاد

غالبًا ما تدور الألعاب في عالم يحتوي على بعض الخصائص الهندسية، إما ثنائية الأبعاد أو ثلاثية الأبعاد. في هذه الحالة، يمكن أن يزيد وضع الصوت الاستيريو بشكل كبير من التجربة الغامرة. لحسن الحظ، تأتي واجهة برمجة تطبيقات Web Audio مزودة بميزات مدمجة للصوت الموضعي السريع والجاهزة للاستخدام. بالمناسبة، يجب التأكد من وجود مكبرات صوت استيريو (يفضل أن تكون سماعات الرأس) للمثال التالي فهمًا.

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

PositionSample.prototype.changePosition = function(position) {
    // Position coordinates are in normalized canvas coordinates
    // with -0.5 < x, y < 0.5
    if (position) {
    if (!this.isPlaying) {
        this.play();
    }
    var mul = 2;
    var x = position.x / this.size.width;
    var y = -position.y / this.size.height;
    this.panner.setPosition(x - mul, y - mul, -0.5);
    } else {
    this.stop();
    }
};

إليك بعض النقاط التي يجب معرفتها عن تعامل Web Audio مع البث المكاني:

  • يكون المستمع في الأصل (0، 0، 0) تلقائيًا.
  • تعد واجهات برمجة تطبيقات Web Audio الموضعية غير موحدة، لذلك قدمت مضاعفًا لجعل الصوت التجريبي أفضل.
  • يستخدم Web Audio الإحداثيات الديكارتية (على عكس معظم أنظمة رسومات الكمبيوتر). هذا هو السبب في أنني أقوم بتبديل المحور ص في المقتطف أعلاه

إعدادات متقدّمة: أقماع صوتية

يتميز نموذج تحديد موضع الإعلان بالفعالية الكبيرة والأكثر تطورًا، ويعتمد بشكل كبير على OpenAL. ولمزيد من التفاصيل، يرجى الاطّلاع على القسمين 3 و4 من المواصفات المرتبطة أعلاه.

نموذج الموضع

هناك أداة AudioListener واحدة مرفقة بسياق Web Audio API والتي يمكن ضبطها في الفضاء من خلال الموضع والاتجاه. يمكن تمرير كل مصدر من خلال AudioPannerNode، وهو يحدّد مكانية صوت الإدخال. تحتوي عقدة التحريك على موضع واتجاه، بالإضافة إلى نموذج مسافة واتجاه.

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

var panner = context.createPanner();
panner.coneOuterGain = 0.5;
panner.coneOuterAngle = 180;
panner.coneInnerAngle = 0;

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

للاطّلاع على مزيد من المعلومات حول هذا الموضوع، اقرأ هذا البرنامج التعليمي المفصّل حول [المزج بين الصوت الموضعي وWebgl][webgl].

تأثيرات الغرف والفلاتر

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

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

لمزيد من المعلومات حول كيفية إنشاء استجابات الانفعال من بيئة معيّنة، يُرجى قراءة قسم "إعداد التسجيل" ضمن جزء Convolution من مواصفات Web Audio API.

والأهم من ذلك بالنسبة إلى أغراضنا، توفّر واجهة برمجة التطبيقات Web Audio API طريقة سهلة لتطبيق هذه الاستجابات الانفعالات على أصواتنا باستخدام أداة ConvolverNode.

// Make a source node for the sample.
var source = context.createBufferSource();
source.buffer = this.buffer;
// Make a convolver node for the impulse response.
var convolver = context.createConvolver();
convolver.buffer = this.impulseResponseBuffer;
// Connect the graph.
source.connect(convolver);
convolver.connect(context.destination);

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

العد التنازلي الأخير

بما أنك صممت لعبة وضبطت إعدادات الصوت الموضعي، وأصبح لديك الآن عدد كبير من عُقد الصوت في الرسم البياني، ويتم تشغيلها كلها في الوقت نفسه. رائع، ولكن لا يزال هناك شيء آخر يجب مراعاته:

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

اقتصاص

إليك مثال حقيقي على الاقتصاص بشكل عملي. الشكل الموجي يبدو سيئًا:

اقتصاص

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

رصد الاقتصاص

من المنظور التقني، تحدث عملية الاقتصاص عندما تتجاوز قيمة الإشارة في أي قناة النطاق الصحيح، أي بين -1 و1. بمجرد اكتشاف ذلك، من المفيد إعطاء ملاحظات مرئية أن ذلك يحدث. لإجراء ذلك بشكل موثوق، ضَع JavaScriptAudioNode في الرسم البياني. سيتم إعداد الرسم البياني الصوتي كما يلي:

// Assume entire sound output is being piped through the mix node.
var meter = context.createJavaScriptNode(2048, 1, 1);
meter.onaudioprocess = processAudio;
mix.connect(meter);
meter.connect(context.destination);

ويمكن رصد الاقتصاص في معالِج processAudio التالي:

function processAudio(e) {
    var buffer = e.inputBuffer.getChannelData(0);

    var isClipping = false;
    // Iterate through buffer to check if any of the |values| exceeds 1.
    for (var i = 0; i < buffer.length; i++) {
    var absValue = Math.abs(buffer[i]);
    if (absValue >= 1) {
        isClipping = true;
        break;
    }
    }
}

بشكل عام، يجب عدم الإفراط في استخدام JavaScriptAudioNode لأسباب تتعلّق بالأداء. في هذه الحالة، قد يؤدي تطبيق بديل لقياس الأداء إلى استطلاع قيمة RealtimeAnalyserNode في الرسم البياني الصوتي لـ getByteFrequencyData في وقت العرض، على النحو الذي يتم تحديده في requestAnimationFrame. هذا الأسلوب أكثر فعالية، ولكنه يفوته معظم الإشارة (بما في ذلك الأماكن التي يُحتمل أن يتم فيها المقاطع)، نظرًا لأن العرض يتم 60 مرة في الثانية كحدّ أقصى، في حين تتغير إشارة الصوت بسرعة أكبر بكثير.

ونظرًا إلى أهمية رصد المقاطع، من المرجّح أن نرى عقدة MeterNode Web Audio API مضمّنة في المستقبل.

منع الاقتصاص

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

أضف القليل من السكر

تُستخدم الضواغط بشكل شائع في إنتاج الموسيقى والألعاب لضبط الإشارة والتحكم في الارتفاعات في الإشارة بشكل عام. تتوفّر هذه الوظيفة في عالم Web Audio من خلال DynamicsCompressorNode، والتي يمكن إدراجها في الرسم البياني للصوت لتقديم صوت أعلى وأكثر ثراءً واكتمالاً، والمساعدة في الاقتصاص أيضًا. مع اقتباس المواصفات مباشرة، هذه العقدة

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

لا يتطلب تنفيذ ذلك سوى تضمين دالة Dynamics pressorNode في الرسم البياني للصوت كعقدة أخيرة قبل الوجهة:

// Assume the output is all going through the mix node.
var compressor = context.createDynamicsCompressor();
mix.connect(compressor);
compressor.connect(context.destination);

للحصول على مزيد من التفاصيل حول الضغط الديناميكي، يمكنك الاطّلاع على هذه المقالة على Wikipedia للحصول على معلومات مفيدة.

للتلخيص، استمع بعناية لقصها ومنعها من خلال إدخال عقدة كسب رئيسية. ثم اربط المزيج بالكامل باستخدام عقدة ضاغط ديناميكي. قد يبدو الرسم البياني للصوت كما يلي:

النتيجة النهائية

الخلاصة

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

للحصول على معلومات إضافية حول Web Audio، يمكنك الاطّلاع على مقالة البدء التمهيدية. وإذا كان لديك أي سؤال، يمكنك الاطّلاع على الإجابة عنه في الأسئلة الشائعة حول المحتوى الصوتي على الويب. وأخيرًا، إذا كان لديك أسئلة إضافية، يمكنك طرحها على Stack Overflow باستخدام علامة web-audio.

قبل تسجيل الخروج، أودّ أن أترككم مع بعض الاستخدامات الرائعة لواجهة Web Audio API في الألعاب الحقيقية اليوم: