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

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

Web Audio API הוא ממשק JavaScript API ברמה גבוהה לעיבוד ולסינתוז אודיו באפליקציות אינטרנט. המטרה של ה-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 מקודד באופן אסינכרוני (בלי לחסום את שרשור הביצוע הראשי של 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;

מעבר הדרגתי בין שני צלילים

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

ניתן לעשות זאת באמצעות תרשים האודיו הבא:

תרשים אודיו עם שני מקורות שמחוברים דרך צומתי רווח
גרף אודיו עם שני מקורות שמחוברים באמצעות צמתים של הגברה

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

סוגי המסננים הנתמכים כוללים:

  • מסנן כרטיסים נמוכים
  • מסנן מסוג High pass
  • מסנן תדר פס
  • פילטר מדף נמוך
  • פילטר של מדף גבוה
  • מסנן שיא
  • מסנן צולב
  • מסנן 'כל הכרטיסים'

כל המסננים כוללים פרמטרים שאפשר להשתמש בהם כדי לציין את מידת השיפור, את התדירות שבה רוצים להחיל את המסנן ואת גורם האיכות. הפילטר עם הפס הנמוך שומר על טווח התדרים הנמוך יותר, אבל הוא מבטל תדרים גבוהים. נקודת ההפסקה נקבעת על סמך ערך התדירות, וגורם 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, כלי לחיבור קטעי אודיו בדפדפן שמשתמש בקישור קבוע (permalink) של SoundCloud.
  • ToneCraft, תוכנה ליצירת סדרת צלילים שבה הצלילים נוצרים על ידי יצירת ערימות של בלוקים תלת-ממדיים.
  • Plink, משחק שיתופי ליצירת מוזיקה באמצעות Web Audio ו-Web Sockets.