دراسة حالة - قصة عن لعبة HTML5 باستخدام Web Audio

اللاعبون الميدانيون

لقطة شاشة لـ Fieldrunners
لقطة شاشة لـ Fieldrunners

لعبة Fieldrunners هي لعبة حائزة على جوائز مصممة للدفاع عن البرج، وقد تم إطلاقها في الأصل لأجهزة iPhone في عام 2008. ومنذ ذلك الحين، تم نقلها إلى العديد من المنصات الأخرى. وكان متصفّح Chrome من أحدث الأنظمة الأساسية في تشرين الأول (أكتوبر) 2011. كانت كيفية تشغيل الصوت إحدى تحديات نقل Fieldrunners إلى نظام HTML5.

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

ظهور بعض التحديات

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

طبيعة AudioBufferSourceNodes

AudioBufferSourceNodes هو طريقتك الأساسية لتشغيل الأصوات باستخدام Web Audio. من المهم جدًا أن نفهم أنها كائن تُستخدم لمرة واحدة. أنشئ AudioBufferSourceNode ، وعيّنه كمخزن مؤقت، ووصله بالرسم البياني، وشغله باستخدام noteOn أو noteGrainOn. بعد ذلك يمكنك استدعاء note Off لإيقاف التشغيل، ولكن لن تتمكن من تشغيل المصدر مرة أخرى عن طريق استدعاء noteOn أو noteGrainOn - أنه عليك إنشاء AudioBufferSourceNode آخر. يمكنك إعادة استخدام كائن AudioBuffer الأساسي نفسه، ولكن (في الواقع، يمكنك الحصول على عدة عُقد نشطة لـ AudioBufferSourceNodes والتي تشير إلى مثيل AudioBuffer نفسه). يمكنك العثور على مقتطف تشغيل من إطلاق لعبة Fieldrunners في لعبة إطلاق لعبة تجريبية.

المحتوى لا يتم تخزينه مؤقتًا

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

كتم الصوت عندما تكون خارج التركيز

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

جارٍ إيقاف الأصوات مؤقتًا

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

بنية بسيطة لعُقدة صوت الويب

تمتلك لعبة Fieldrunners نموذجًا صوتيًا بسيطًا جدًا. ويمكن أن يدعم هذا النموذج مجموعة الخصائص التالية:

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

ولتحقيق الميزات المذكورة أعلاه باستخدام Web Audio، تم استخدام 3 من العُقد المحتملة المتوفرة: DestinationNode وGainNode و AudioBufferSourceNode. تُشغِّل AudioBufferSourceNodes الأصوات. تربط عُقدة GainNodes AudioBufferSourceNodes معًا. تُشغّل DestinationNode التي يتم إنشاؤها بواسطة سياق Web Audio ، وتسمى الوجهة، الأصوات للمشغل. تشتمل تقنية Web Audio على العديد من أنواع العُقد، ولكن باستخدامها فقط، يمكننا إنشاء رسم بياني بسيط للغاية للأصوات في أي لعبة.

رسم بياني للعقدة

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

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

function AudioManager() {
  // map for loaded sounds
  this.sounds = {};

  // create our permanent nodes
  this.nodes = {
    destination: this.audioContext.destination,
    masterGain: this.audioContext.createGain(),

    backgroundMusicGain: this.audioContext.createGain(),

    coreEffectsGain: this.audioContext.createGain(),
    effectsGain: this.audioContext.createGain(),
    pausedEffectsGain: this.audioContext.createGain()
  };

  // and setup the graph
  this.nodes.masterGain.connect( this.nodes.destination );

  this.nodes.backgroundMusicGain.connect( this.nodes.masterGain );

  this.nodes.coreEffectsGain.connect( this.nodes.masterGain );
  this.nodes.effectsGain.connect( this.nodes.coreEffectsGain );
  this.nodes.pausedEffectsGain.connect( this.nodes.coreEffectsGain );
}

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

function setArbitraryVolume() {
  var musicGainNode = this.nodes.backgroundMusicGain;

  // set music volume to 50%
  musicGainNode.gain.value = 0.5;
}

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

function arbitraryCrossfade( track1, track2 ) {
  track1.gain.linearRampToValueAtTime( 0, 1 );
  track2.gain.linearRampToValueAtTime( 1, 1 );
}

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

إيقاف الأصوات مؤقتًا

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

AudioManager.prototype.pauseEffects = function() {
  this.nodes.effectsGain.disconnect();
}

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

AudioManager.prototype.resumeEffects = function() {
  this.nodes.effectsGain.connect( this.nodes.coreEffectsGain );
}

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

AudioManager.prototype.pauseEffects = function() {
  this.nodes.effectsGain.disconnect();

  var now = Date.now();
  for ( var name in this.sounds ) {
    var sound = this.sounds[ name ];

    if ( !sound.ignorePause && ( now - sound.source.noteOnAt < sound.buffer.duration * 1000 ) ) {
      sound.pausedAt = now - sound.source.noteOnAt;
      sound.source.noteOff();
    }
  }
}

AudioManager.prototype.resumeEffects = function() {
  this.nodes.effectsGain.connect( this.nodes.coreEffectsGain );

  var now = Date.now();
  for ( var name in this.sounds ) {
    if ( sound.pausedAt ) {
      this.play( sound.name );
      delete sound.pausedAt;
    }
  }
};

لو عرفنا ذلك من قبل، أننا نسيء استخدام خطأ، لكان بنية الرمز الصوتي مختلفة تمامًا. وبالتالي، أثر ذلك في عدد من أقسام هذه المقالة. وهو له تأثير مباشر هنا، ولكن أيضًا في مقتطفات الرمز الخاصة بنا في حدثَي Loing Focus وGive Me a Beat. وتتطلّب معرفة طريقة عمل هذه الطريقة إجراء تغييرات في كلّ من الرسم البياني للعُقد في Fieldrunners (لأنّنا أنشأنا عُقدًا لاختصار التشغيل) والرمز الإضافي الذي سيسجِّل حالات الإيقاف المؤقت ويوفر الحالات التي لا يتم إجراؤها في Web Audio بمفرده.

فقدان التركيز

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

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

function AudioManager() {
  // map and node setup
  // ...

  // disable all sound when on other tabs
  var self = this;
  window.addEventListener( 'webkitvisibilitychange', function( e ) {
    if ( document.webkitHidden ) {
      self.nodes.masterGain.disconnect();

      // As noted in Pausing Sounds disconnecting isn't enough.
      // For Fieldrunners calling our new pauseEffects method would be
      // enough to accomplish that, though we may still need some logic
      // to not resume if already paused.
      self.pauseEffects();
    } else {
      self.nodes.masterGain.connect( this.nodes.destination );
      self.resumeEffects();
    }
  });
}

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

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

أريد سماع إيقاع

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

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

بالنسبة إلى مصممي الحقول، يبدو الأمر على النحو التالي:

AudioManager.prototype.play = function( options ) {
  var now = Date.now(),
    // pull from a map of loaded audio buffers
    sound = this.sounds[ options.name ],
    channel,
    source,
    resumeSource;

  if ( !sound ) {
    return;
  }

  if ( sound.source ) {
    var source = sound.source;
    if ( !options.loop && now - source.noteOnAt > sound.buffer.duration * 1000 ) {
      // discard the previous source node
      source.stop( 0 );
      source.disconnect();
    } else {
      return;
    }
  }

  source = this.audioContext.createBufferSource();
  sound.source = source;
  // track when the source is started to know if it should still be playing
  source.noteOnAt = now;

  // help with pausing
  sound.ignorePause = !!options.ignorePause;

  if ( options.ignorePause ) {
    channel = this.nodes.pausedEffectsGain;
  } else {
    channel = this.nodes.effectsGain;
  }

  source.buffer = sound.buffer;
  source.connect( channel );
  source.loop = options.loop || false;

  // Fieldrunners' current code doesn't consider sound.pausedAt.
  // This is an added section to assist the new pausing code.
  if ( sound.pausedAt ) {
    source.start( ( sound.buffer.duration * 1000 - sound.pausedAt ) / 1000 );
    source.noteOnAt = now + sound.buffer.duration * 1000 - sound.pausedAt;

    // if you needed to precisely stop sounds, you'd want to store this
    resumeSource = this.audioContext.createBufferSource();
    resumeSource.buffer = sound.buffer;
    resumeSource.connect( channel );
    resumeSource.start(
      0,
      sound.pausedAt,
      sound.buffer.duration - sound.pausedAt / 1000
    );
  } else {
    // start play immediately with a value of 0 or less
    source.start( 0 );
  }
}

البث المباشر كبير جدًا

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

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

AudioManager.prototype.load = function( options ) {
  var xhr,
      // pull from a map of name, object pairs
      sound = this.sounds[ options.name ];

  if ( sound ) {
    // this is a great spot to add success methods to a list or use promises
    // for handling the load event or call success if already loaded
    if ( sound.buffer && options.success ) {
      options.success( options.name );
    } else if ( options.success ) {
      sound.success.push( options.success );
    }

    // one buffer is enough so shortcut here
    return;
  }

  sound = {
    name: options.name,
    buffer: null,
    source: null,
    success: ( options.success ? [ options.success ] : [] )
  };
  this.sounds[ options.name ] = sound;

  xhr = new XMLHttpRequest();
  xhr.open( 'GET', options.path, true );
  xhr.responseType = 'arraybuffer';
  xhr.onload = function( e ) {
    sound.buffer = self._context.createBuffer( xhr.response, false );

    // call all waiting handlers
    sound.success.forEach( function( success ) {
      success( sound.name );
    });
    delete sound.success;
  };
  xhr.onerror = function( e ) {

    // failures are uncommon but you want to do deal with them

  };
  xhr.send();
}

ملخّص

كانت لعبة Fieldrunners فرصة رائعة لإدخالها إلى Chrome وHTML5. خارج نطاق العمل الخاص بها، تجلب الآلاف من خطوط C++ إلى جافا سكريبت، وهناك بعض المعضلات والقرارات المثيرة للاهتمام المتعلقة بـ HTML5. لتكرار أحدها إذا لم يكن أي منها آخر، تكون AudioBufferSourceNodes تستخدم الكائنات مرة واحدة. يمكنك إنشاء الملفات وإرفاق مخزن مؤقت للصوت وتوصيله بالرسم البياني للصوت على Web Audio ولعبه باستخدام noteOn أو noteGrainOn. هل تريد تشغيل هذا الصوت مرة أخرى؟ ثم أنشئ AudioBufferSourceNode آخر.