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

לפני רכיב ה-HTML5‏ <audio>, היה צורך ב-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.

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

הוספת אפקט פשוט של מסנן לצליל

תרשים זרימה פשוט שמציג אודיו שזורם מ-Source אל BiquadFilterNode אל Destination.

‫Web Audio API מאפשר להעביר צליל מצומת אודיו אחת לצומת אודיו אחרת, וכך ליצור שרשרת מורכבת של מעבדים להוספת אפקטים מורכבים לצורות הצליל.

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

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

  • פילטר מעביר נמוך
  • מסנן מעביר גבוה
  • מסנן פס מעביר
  • פילטר מדף נמוך
  • מסנן מדף גבוה
  • פילטר לחידוד התמונה
  • מסנן Notch
  • מסנן להעברת הכול

כל המסננים כוללים פרמטרים לציון סכום מסוים של עלייה, התדירות שבה המסנן יופעל וגורם איכות. המסנן למעבר נמוך שומר על טווח התדרים הנמוך, אבל מבטל תדרים גבוהים. נקודת ההתנתקות נקבעת לפי ערך התדירות, ומקדם האיכות הוא חסר יחידות וקובע את צורת הגרף. ההגברה משפיעה רק על פילטרים מסוימים, כמו פילטרים של מדף נמוך ופילטרים של שיא, ולא על פילטרים של מעבר נמוך.

נניח שאנחנו רוצים להגדיר מסנן פשוט להעברת תדרים נמוכים כדי לחלץ רק את הבסים מדגימת קול:

// 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);

האזנה נוספת

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

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

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