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

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

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

بدء استخدام AudioContext

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

يمكن أن يدعم مثيل واحد من 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 لـ جلب الملفات الصوتية.

تتيح واجهة برمجة التطبيقات تحميل بيانات الملفات الصوتية بتنسيقات متعددة، مثل WAV وMP3 والترميز المتقدّم للصوت و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 من أجل التحكّم في مستوى الصوت:

مخطط انسيابي بسيط يوضّح تدفّق الصوت من المصدر إلى GainNode إلى الوجهة

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

// 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.

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

مخطط انسيابي بسيط يوضّح تدفّق الصوت من &quot;المصدر&quot; إلى BiquadFilterNode إلى &quot;الوجهة&quot;

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

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

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

  • فلتر الترددات المنخفضة
  • فلتر الترددات العالية
  • فلتر الترددات المتوسطة
  • فلتر الرف المنخفض
  • فلتر الرف العالي
  • فلتر الذروة
  • فلتر الحز
  • فلتر جميع الترددات

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

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

// 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 في رابط رمز المصدر أعلاه.

أخيرًا، يُرجى العِلم أنّ رمز النموذج يتيح لك ربط الفلتر وفصله، ما يؤدي إلى تغيير الرسم البياني لـ AudioContext بشكل ديناميكي. يمكننا فصل 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.