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

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

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

بدء استخدام AudioContext

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

يمكن أن يتيح مثيل واحد من AudioContext استخدام مصادر صوت متعددة ورسوم بيانية صوتية معقّدة، لذا سنحتاج إلى مثيل واحد فقط من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 المثيرة للاهتمام، مثل إنشاء AudioNodes وفك ترميز بيانات الملفات الصوتية، هي طُرق AudioContext.

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

تستخدِم واجهة برمجة التطبيقات Web Audio API عنصر AudioBuffer للأصوات القصيرة أو المتوسطة الطول. يتمثل النهج الأساسي في استخدام XMLHttpRequest ل retrievingملفّات الصوت.

تتيح واجهة برمجة التطبيقات تحميل بيانات الملفات الصوتية بتنسيقات متعددة، مثل 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.

بعد استلام بيانات ملف الصوت (غير المُشفَّرة)، يمكن الاحتفاظ بها لفك تشفيرها لاحقًا، أو يمكن فك تشفيرها على الفور باستخدام الأسلوب decodeAudioData() في AudioContext. تأخذ هذه الطريقة 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 API

بالطبع، من الأفضل إنشاء نظام تحميل أكثر عمومية غير مُبرمَج لتحميل هذا الصوت تحديدًا. هناك طرق عديدة للتعامل مع العديد من الأصوات القصيرة إلى متوسطة الطول التي قد يستخدمها تطبيق أو لعبة صوتية - إليك طريقة باستخدام 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 API، يمكننا استخدام واجهة 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 API مجموعة ملائمة من طرق 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);

بشكل عام، يجب تعديل عناصر التحكّم في الترددات للعمل على قياس logarithmic scale لأنّ السمع البشري نفسه يعمل على المبدأ نفسه (أي أنّ 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