بدء استخدام Web Audio API

قبل استخدام عنصر <audio> HTML5، كان يلزم استخدام Flash أو مكون إضافي آخر لكسر كتم الصوت على الويب. على الرغم من أنّ الصوت على الويب لم يعُد يتطلب مكوّنًا إضافيًا، تفرض علامة الصوت قيودًا كبيرة على تنفيذ الألعاب المتطوّرة والتطبيقات التفاعلية.

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

بدء استخدام AudioContext

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

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

ينشئ المقتطف التالي AudioContext:

var context;
window.addEventListener('load', init, false);
function init() {
    try {
    context = new AudioContext();
    }
    catch(e) {
    alert('Web Audio API is not supported in this browser');
    }
}

وبالنسبة إلى المتصفّحات القديمة المستندة إلى WebKit، يمكنك استخدام البادئة webkit، كما هي الحال في webkitAudioContext.

العديد من وظائف واجهة برمجة التطبيقات Web Audio API، مثل إنشاء AudioNode وفك ترميز بيانات الملفات الصوتية، هي طرق تستخدم AudioContext.

جارٍ تحميل الأصوات

تستخدم واجهة برمجة تطبيقات Web Audio Buffer للأصوات القصيرة إلى المتوسطة الطول. الطريقة الأساسية هي استخدام XMLHttpRequest لجلب الملفات الصوتية.

تتيح واجهة برمجة التطبيقات تحميل بيانات الملفات الصوتية بتنسيقات متعددة، مثل WAV وMP3 وAAC وOGG وتنسيقات أخرى. يختلف توافق المتصفّح مع تنسيقات الصوت المختلفة.

يعرض المقتطف التالي تحميل عيّنة صوتية:

var dogBarkingBuffer = null;
var context = new AudioContext();

function loadDogSound(url) {
    var request = new XMLHttpRequest();
    request.open('GET', url, true);
    request.responseType = 'arraybuffer';

    // Decode asynchronously
    request.onload = function() {
    context.decodeAudioData(request.response, function(buffer) {
        dogBarkingBuffer = buffer;
    }, onError);
    }
    request.send();
}

تكون بيانات الملف الصوتي ثنائية (وليست نصيّة)، لذا تم ضبط responseType للطلب على 'arraybuffer'. لمزيد من المعلومات حول ArrayBuffers، يمكنك الاطّلاع على هذه المقالة حول XHR2.

بعد تلقّي بيانات الملف الصوتي (غير فك ترميزها)، يمكن الاحتفاظ بها لفك ترميزها لاحقًا أو يمكن فك ترميزها على الفور باستخدام طريقة AudioContext decodeAudioData(). تستخدم هذه الطريقة ArrayBuffer من بيانات الملف الصوتي المخزّنة في request.response وفك ترميزها بشكل غير متزامن (بدون حظر سلسلة التعليمات الرئيسية لتنفيذ JavaScript).

عند انتهاء decodeAudioData()، تستدعي دالة معاودة الاتصال التي توفّر بيانات صوت PCM التي تم فك ترميزها على أنّها AudioBuffer.

جارٍ تشغيل الأصوات

رسم بياني صوتي بسيط
رسم بياني صوتي بسيط

بعد تحميل AudioBuffers واحد أو أكثر، نصبح جاهزين لتشغيل الأصوات. لنفترض أننا حمّلنا للتوّ AudioBuffer مع صوت نباح كلب وأنّ التحميل قد انتهى. ثم يمكننا تشغيل هذا المورد الاحتياطي بالتعليمة البرمجية التالية.

var context = new AudioContext();

function playSound(buffer) {
    var source = context.createBufferSource(); // creates a sound source
    source.buffer = buffer;                    // tell the source which sound to play
    source.connect(context.destination);       // connect the source to the context's destination (the speakers)
    source.noteOn(0);                          // play the source now
}

يمكن استدعاء وظيفة playSound() هذه في كل مرة يضغط فيها أحد الأشخاص على مفتاح أو ينقر على عنصر بالماوس.

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

تجريد واجهة برمجة التطبيقات Web Audio

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

في ما يلي مثال على كيفية استخدام الفئة BufferLoader. لنُنشئ اثنين من AudioBuffers. وبمجرد تحميلهما، لنشغلهما في الوقت نفسه.

window.onload = init;
var context;
var bufferLoader;

function init() {
    context = new AudioContext();

    bufferLoader = new BufferLoader(
    context,
    [
        '../sounds/hyper-reality/br-jam-loop.wav',
        '../sounds/hyper-reality/laughter.wav',
    ],
    finishedLoading
    );

    bufferLoader.load();
}

function finishedLoading(bufferList) {
    // Create two sources and play them both together.
    var source1 = context.createBufferSource();
    var source2 = context.createBufferSource();
    source1.buffer = bufferList[0];
    source2.buffer = bufferList[1];

    source1.connect(context.destination);
    source2.connect(context.destination);
    source1.noteOn(0);
    source2.noteOn(0);
}

التعامل مع الوقت: تشغيل الأصوات بالإيقاع

تسمح Web Audio API للمطوّرين بجدولة عمليات التشغيل بدقة. لإثبات ذلك، لنقم بإعداد مسار إيقاع بسيط. من المحتمل أن يكون نمط الطبل الأكثر شهرة هو ما يلي:

نمط بسيط لطبلة صخرية
على شكل طبلة صخرية بسيطة

يتم فيه عزف نوتة واحدة في كل ثمانية نوتة، والركلة والرنين بالتناوب كل ربع سنة، في 4/4 مرات.

لنفترض أنّنا حمَّلنا المخازن الاحتياطية kick وsnare وhihat، يُعتبر الرمز البرمجي لإجراء ذلك بسيطًا:

for (var bar = 0; bar < 2; bar++) {
    var time = startTime + bar * 8 * eighthNoteTime;
    // Play the bass (kick) drum on beats 1, 5
    playSound(kick, time);
    playSound(kick, time + 4 * eighthNoteTime);

    // Play the snare drum on beats 3, 7
    playSound(snare, time + 2 * eighthNoteTime);
    playSound(snare, time + 6 * eighthNoteTime);

    // Play the hi-hat every eighth note.
    for (var i = 0; i < 8; ++i) {
    playSound(hihat, time + i * eighthNoteTime);
    }
}

هنا، نقوم بتكرار واحد فقط بدلاً من التكرار غير المحدود الذي نراه في الورقة الموسيقية. الدالة playSound هي طريقة تُشغِّل مخزنًا مؤقتًا في وقت محدّد، على النحو التالي:

function playSound(buffer, time) {
    var source = context.createBufferSource();
    source.buffer = buffer;
    source.connect(context.destination);
    source.noteOn(time);
}

تغيير مستوى الصوت

من العمليات الأساسية التي قد ترغب في إجرائها للحصول على صوت هي تغيير مستوى الصوت. باستخدام Web Audio API، يمكننا توجيه مصدرنا إلى وجهته من خلال AudioGainNode من أجل معالجة الحجم:

رسم بياني صوتي مع عقدة جمع
رسم بياني صوتي مع عقدة ربح

يمكن تنفيذ إعداد الاتصال هذا على النحو التالي:

// Create a gain node.
var gainNode = context.createGainNode();
// Connect the source to the gain node.
source.connect(gainNode);
// Connect the gain node to the destination.
gainNode.connect(context.destination);

بعد إعداد الرسم البياني، يمكنك تغيير مستوى الصوت آليًا من خلال معالجة gainNode.gain.value على النحو التالي:

// Reduce the volume.
gainNode.gain.value = 0.5;

تلاشي متداخل بين صوتين

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

يمكن إجراء ذلك باستخدام الرسم البياني الصوتي التالي:

رسم بياني صوتي مع مصدرين متصلين من خلال عُقد كسب
رسم بياني صوتي يتضمن مصدرين مرتبطين من خلال عُقد الجمع

لإعداد ذلك، ننشئ ببساطة AudioGainNodes ونربط كل مصدر من خلال العُقد، باستخدام شيء مثل هذه الدالة:

function createSource(buffer) {
    var source = context.createBufferSource();
    // Create a gain node.
    var gainNode = context.createGainNode();
    source.buffer = buffer;
    // Turn on looping.
    source.loop = true;
    // Connect source to gain.
    source.connect(gainNode);
    // Connect gain to destination.
    gainNode.connect(context.destination);

    return {
    source: source,
    gainNode: gainNode
    };
}

تلاشٍ متقاطع متساوي الطاقة

يُظهر نهج التلاشي المتقاطع الخطي انخفاضًا في الحجم أثناء التنقل بين العينات.

تلاشٍ متقاطع خطي
تأثير التلاشي المتقاطع الخطي

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

تلاشٍ متقاطع متساوي القوة.
تأثير القوة المتقاطعة على مبدأ القوة

تلاشي متداخل لقوائم التشغيل

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

وبالتالي، وفقًا لقائمة تشغيل، يمكننا الانتقال بين المقاطع الصوتية من خلال جدولة خفض نسبة الزيادة في المقطع الصوتي الذي يتم تشغيله حاليًا، وزيادة الزيادة في المقطع التالي، وكلاهما قبل انتهاء تشغيل المقطع الصوتي الحالي بقليل:

function playHelper(bufferNow, bufferLater) {
    var playNow = createSource(bufferNow);
    var source = playNow.source;
    var gainNode = playNow.gainNode;
    var duration = bufferNow.duration;
    var currTime = context.currentTime;
    // Fade the playNow track in.
    gainNode.gain.linearRampToValueAtTime(0, currTime);
    gainNode.gain.linearRampToValueAtTime(1, currTime + ctx.FADE_TIME);
    // Play the playNow track.
    source.noteOn(0);
    // At the end of the track, fade it out.
    gainNode.gain.linearRampToValueAtTime(1, currTime + duration-ctx.FADE_TIME);
    gainNode.gain.linearRampToValueAtTime(0, currTime + duration);
    // Schedule a recursive track change with the tracks swapped.
    var recurse = arguments.callee;
    ctx.timer = setTimeout(function() {
    recurse(bufferLater, bufferNow);
    }, (duration - ctx.FADE_TIME) - 1000);
}

توفر واجهة برمجة تطبيقات Web Audio مجموعة ملائمة من طُرق RampToValue لتغيير قيمة معلَمة تدريجيًا، مثل linearRampToValueAtTime وexponentialRampToValueAtTime.

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

تطبيق تأثير فلتر بسيط على الصوت

رسم بياني للصوت مع BiquadFilterNode
رسم بياني صوتي مع BiquadFilterNode

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

وإحدى الطرق المتاحة لتنفيذ ذلك هي وضع BiquadFilterNode بين مصدر الصوت والوجهة. يمكن لهذا النوع من العُقد الصوتية إجراء مجموعة متنوعة من الفلاتر ذات الترتيب المنخفض التي يمكن استخدامها لإنشاء معادِلات للرسم

تشمل أنواع الفلاتر المتاحة ما يلي:

  • فلتر التمرير المنخفض
  • فلتر التمرير العالي
  • فلتر تمرير السوار
  • فلتر الرف المنخفض
  • فلتر الرف المرتفع
  • فلتر الوصول إلى الذروة
  • فلتر من المستوى الأعلى
  • فلتر كل البطاقات

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

لنبدأ بإعداد فلتر بسيط للاجتياز المنخفض لاستخراج القواعد فقط من عيّنة صوتية:

// Create the filter
var filter = context.createBiquadFilter();
// Create the audio graph.
source.connect(filter);
filter.connect(context.destination);
// Create and specify parameters for the low-pass filter.
filter.type = 0; // Low-pass filter. See BiquadFilterNode docs
filter.frequency.value = 440; // Set cutoff to 440 HZ
// Playback the sound.
source.noteOn(0);

وبشكل عام، يجب تعديل عناصر التحكم في التردد لتعمل على المقياس اللوغاريتمي لأنّ السمع البشري نفسه يعمل على المبدأ نفسه (أي A4 هو 440 هرتز، وA5 هو 880 هرتز). لمزيد من التفاصيل، اطّلِع على دالة "FilterSample.changeFrequency" في رابط رمز المصدر أعلاه.

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

// Disconnect the source and filter.
source.disconnect(0);
filter.disconnect(0);
// Connect the source directly.
source.connect(context.destination);

محتوى إضافي للاستماع إلى الموسيقى

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

إذا كنت تبحث عن الإلهام، اعلم أن العديد من المطورين قد ابتكروا عملًا رائعًا بالفعل باستخدام Web Audio API. من بين أفضل ما يلي:

  • AudioJedit، أداة لربط الصوت في المتصفّح تستخدم الروابط الثابتة في SoundCloud.
  • ToneCraft، أداة ترتيب الأصوات التي يتم فيها إنشاء الأصوات من خلال تكديس كتل ثلاثية الأبعاد.
  • Plink، وهي لعبة تعاونية لتأليف الموسيقى باستخدام Web Audio وWeb Sockets.