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

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

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

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

תבנית תופים פשוטה בסגנון רוק
תבנית תופים פשוטה לרוק

שבו תו ה-hi-hat מופעל בכל תו שמיני, והתופים העמוקים והתופים הצדדיים מופעלים לסירוגין בכל תו רבע, בקצב של 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;

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

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