תחילת העבודה עם Web Audio API

לפני אלמנט ה-HTML5 <audio>, היה צורך ב-Flash או בפלאגין אחר כדי לשבור את שתיקת הרשת. בעוד שהאודיו באינטרנט כבר לא מחייב פלאגין, תג האודיו מגביל מאוד את הטמעת המשחקים המתוחכמים והאפליקציות האינטראקטיביות.

ה-Web Audio API הוא ממשק API של JavaScript ברמה גבוהה לעיבוד ולסינתזה של אודיו ביישומי אינטרנט. המטרה של ה-API הזה היא לכלול יכולות שנמצאות במנועי אודיו מודרניים של משחקים, וחלק ממשימות המיקס, העיבוד והסינון שקיימות באפליקציות מודרניות להפקת אודיו במחשב. הנה מבוא עדין לשימוש ב-API העוצמתי הזה.

תחילת העבודה עם 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, כמו יצירת AudioNodes ופענוח נתונים של קובצי אודיו, הן שיטות של AudioContext.

הצלילים בטעינה

ה-Web Audio API משתמש ב-AudioBuffer לצלילים באורך קצר עד בינוני. הגישה הבסיסית היא להשתמש ב-XMLHttpRequest לאחזור קובצי קול.

ה-API תומך בטעינת נתונים של קובצי אודיו בפורמטים שונים, כמו 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 ומפענחת אותו באופן אסינכרוני (בלי לחסום את ה-thread הראשי להפעלה של 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);

באופן כללי, יש לכוונן את בקרות התדירות על מנת שיפעלו בקנה מידה לוגריתמי, מכיוון שהשמיעה האנושית עצמה פועלת על אותו עיקרון (כלומר, A4 הוא 440hz ו-A5 הוא 880hz). לפרטים נוספים, קראו את הפונקציה 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);

האזנה נוספת

למדנו את היסודות של ה-API, כולל טעינה והפעלה של דוגמאות אודיו. יצרנו גרפים של אודיו עם צמתים ומסננים, וגם צלילים מתוזמנים ושינויים בפרמטרים של אודיו כדי לאפשר אפקטים קוליים נפוצים. בשלב הזה אתם מוכנים להתחיל וליצור אפליקציות אודיו מתוקות לאינטרנט!

אם אתם מחפשים השראה, מפתחים רבים כבר יצרו עבודה מצוינת באמצעות Web Audio API. כמה מהמועדפים שלי כוללים:

  • AudioJedit, כלי לפיצול צלילים בדפדפן שמשתמש בקישורים קבועים ב-SoundCloud.
  • ToneCraft, רצף צלילים שבו נוצרים צלילים על ידי סילוק בלוקים תלת-ממדיים.
  • Plink, משחק שיתופי ליצירת מוזיקה באמצעות Web Audio ו-WebSockets.