סיפור של שני שעונים

תזמון מדויק של אודיו באינטרנט

כריס ווילסון
כריס ווילסון

מבוא

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

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

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

המיטב של הזמנים - שעון האודיו באינטרנט

ה-Web Audio API חושף את הגישה לשעון החומרה של מערכת המשנה האודיו. השעון הזה חשוף באובייקט AudioContext באמצעות המאפיין .currentTime כמספר נקודה צפה (floating-point) של שניות מאז יצירת AudioContext. כך שעון זה (שנקרא להלן "שעון אודיו") יכול להיות מדויק מאוד. הוא תוכנן כך שניתן יהיה לציין יישור ברמה של דגימת צליל בודדת, גם עם קצב דגימה גבוה. מכיוון שיש כ-15 ספרות עשרוניות של דיוק ב'כפולה', גם אם שעון האודיו פועל במשך ימים, עדיין אמורים להיות בו הרבה ביטים כדי להצביע על דגימה ספציפית גם עם קצב דגימה גבוה.

שעון האודיו משמש לתזמון פרמטרים ואירועי אודיו ב-Web Audio API – כמובן ל-start() ול-stop(), אבל גם ל-set*ValueAtTime() ב-AudioParams. כך אנחנו יכולים להגדיר מראש אירועי אודיו עם תזמון מדויק מאוד. למעשה, מפתה להגדיר את כל הנתונים ב-Web Audio כזמני התחלה/עצירה - עם זאת, בפועל יש בעיה בכך.

לדוגמה, אפשר לעיין בקטע הקוד המוקטן מהמבוא שלנו ל-Web Audio, שמגדיר שתי עמודות בדפוס hi-hat של תו שמינית:

for (var bar = 0; bar < 2; bar++) {
  var time = startTime + bar * 8 * eighthNoteTime;

  // Play the hi-hat every eighth note.
  for (var i = 0; i < 8; ++i) {
    playSound(hihat, time + i * eighthNoteTime);
  }

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

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

הדברים הגרועים ביותר – שעון JavaScript

יש לנו גם את שעון ה-JavaScript המוכר והאהוב מאוד, המיוצג על ידי Date.now() ו-setTimeout(). הצד הטוב של שעון JavaScript הוא שיש בו שתי שיטות שימושיות מאוד של "call-me-back-later window.setTimeout() " ו-window.setInterval() , שמאפשרות למערכת לקרוא את הקוד שלנו בחזרה בזמנים מסוימים.

הצד הגרוע של שעון ה-JavaScript הוא שהוא לא מדויק במיוחד. בתור התחלה, Date.now() מחזירה ערך באלפיות שנייה – מספר שלם של אלפיות שנייה – כך שהדיוק הטוב ביותר שניתן לקוות הוא אלפית שנייה. זה לא ממש גרוע בהקשרים מוזיקליים מסוימים - אם התו שלכם התחיל אלפית שנייה מוקדם או מאוחר, אולי אפילו לא תשימו לב. אבל אפילו בקצב חומרת אודיו נמוך יחסית של 44.1kHz, הוא קצב של פי 44.1 איטי מדי לשימוש כשעון לתזמון אודיו. חשוב לזכור: הפלת דגימות עלולה להוביל לתקלה באודיו. לכן, אם אנחנו משרשרים דגימות יחד, ייתכן שנצטרך שהן יהיו רציפות בדיוק.

למעשה, המפרט החדש של זמן רזולוציה גבוהה מאפשר לנו לשפר את הדיוק של משך הזמן הנוכחי דרך window.performance.now(); הוא אפילו מיושם (אם כי התחילית שלו מופיעה) בדפדפנים רבים קיימים. הכלי הזה יכול לעזור במצבים מסוימים, למרות שהוא לא ממש רלוונטי לחלק הגרוע ביותר של ממשקי ה-API לתזמון ב-JavaScript.

החלק הגרוע ביותר בממשקי ה-API של תזמון JavaScript הוא שלמרות שהדיוק באלפיות השנייה של Date.now() לא נשמע רע מדי, ניתן להטות את הקריאה החוזרת (callback) של אירועי הטיימר ב-JavaScript (דרך window.setTimeout() או window.setInterval) בקלות על ידי עשרות אלפיות שנייה או יותר על-ידי פריסה, עיבוד, איסוף אשפה, מספר הפעלות של XMLHTTPRequest ומספר קריאות חוזרות (callback) - בקיצור, כל מה שקורה ב-threads. זוכרים איך הזכרתי "אירועי אודיו" שנוכל לתזמן באמצעות Web Audio API? ובכן, כל השרשורים האלה עוברים עיבוד בשרשור נפרד. לכן, גם אם ה-thread הראשי נעצר באופן זמני בביצוע פריסה מורכבת או משימה ארוכה אחרת, האודיו עדיין יתנגן בדיוק בזמן שבו נאמר להם. למעשה, גם אם תעצור בנקודת עצירה בכלי ניפוי הבאגים, שרשור האודיו ימשיך להפעיל אירועים מתוזמנים.

שימוש ב-JavaScript setTimeout() באפליקציות אודיו

מכיוון שה-thread הראשי יכול בקלות להיתקע למשך כמה אלפיות שנייה בכל פעם, מומלץ להשתמש ב-setTimeout של JavaScript כדי להפעיל ישירות אירועי אודיו. הסיבה לכך היא שבמקרה שבו ההערות יופעלו תוך אלפית שנייה בערך, הן יתעכבו למשך זמן רב יותר. והכי גרוע – מה שאמור להיות רצפים קצביים, הם לא יופעלו במרווחים מדויקים, כי התזמון יהיה רגיש לדברים אחרים שקורים בשרשור ה-JavaScript הראשי.

כדי להדגים זאת, כתבתי יישום מטרונום "גרוע" - כלומר, יישום שמשתמש ב-setTimeout ישירות כדי לתזמן הערות - וגם מבצע פריסה רבה. פתחו את האפליקציה, לחצו על 'הפעלה' ושנו את הגודל של החלון במהירות תוך כדי ההפעלות. תוכלו לראות שהתזמון קצת רעיד (אפשר לשמוע שהקצב לא עקבי). "אבל זה מובן!" כמובן, אבל זה לא אומר שזה לא קורה גם בעולם האמיתי. אפילו בממשק משתמש סטטי יחסית יהיו בעיות תזמון ב-setTimeout עקב ממסרים - לדוגמה, שמתי לב ששינוי גודל של החלון במהירות יגרום לתזמון של WebkitSynth, שנמצא אחרת, מצוין, באופן משמעותי. עכשיו נסו לדמיין מה יקרה כשתנסו לגלול בין פרטי מוזיקה מלאים יחד עם האודיו, ותוכלו לדמיין בקלות איך זה ישפיע על אפליקציות מוזיקה מורכבות בעולם האמיתי.

אחת מהשאלות הנפוצות ביותר שאני שומע היא "למה אין לי אפשרות לקבל קריאות חוזרות מאירועי אודיו?" למרות שיכולים להיות שימושים לסוגים האלה של קריאות חוזרות (callback), הם לא יפתרו את הבעיה הספציפית הספציפית הזו. חשוב להבין שהאירועים האלה יופעלו בשרשור ה-JavaScript הראשי, כך שהם יהיו כפופים לכל אותם עיכובים פוטנציאליים שנקבעים על ידי setTimeout; כלומר, הם עלולים להתעכב בגלל עיכובים לא ידועים שהתעכבו.

אז מה אנחנו יכולים לעשות? הדרך הטובה ביותר להתמודד עם תזמון היא להגדיר שיתוף פעולה בין מגבלות זמן ב-JavaScript (setTimeout() , setInterval() או requestAnimationFrame() - מידע נוסף בהמשך) לבין תזמון חומרת האודיו.

השגת תזמון מושלם במבט קדימה

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

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

setTimeout() ואינטראקציה של אירוע אודיו.
setTimeout() ואינטראקציה של אירוע אודיו.

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

למעשה, אנחנו לא רוצים להסתכל קדימה בדיוק לפי המרווח שבין קריאות ל-setTimeout() – אנחנו זקוקים גם לחפיפה מסוימת בתזמון בין קריאת הטיימר הזו לזו הבאה, כדי לתת מענה להתנהגות ה-thread הראשית במקרה הגרוע ביותר. כלומר, במקרה הגרוע ביותר של איסוף אשפה, פריסה, עיבוד או קוד אחר שמתרחש ב-thread הראשי שמעכב את הקריאה הבאה לטיימר. אנחנו צריכים להביא בחשבון גם את הזמן של תזמון בלוק האודיו - כלומר, כמה אודיו מערכת ההפעלה שומרת במאגר הנתונים הזמני, שמשתנה בין מערכות ההפעלה והחומרה, החל מספרות בודדות של אלפיות השנייה ועד כ-50 אלפיות השנייה. לכל קריאה ל-setTimeout() שמוצגת למעלה יש מרווח כחול שמציג את כל טווח הזמנים שבמהלכו המערכת תנסה לתזמן אירועים. לדוגמה, יכול להיות שאירוע האודיו הרביעי שתוזמן בתרשים שלמעלה הופעל "late" אם חיכינו להפעלה עד שהקריאה הבאה ל-setTimeout התרחשה, במקרה שהקריאה ל-setTimeout התרחשה רק כמה אלפיות השנייה מאוחר יותר. בחיים האמיתיים, הרעידות בזמנים כאלה יכולות להיות אפילו קיצוניות יותר, והחפיפה הזו הופכת חשובה אף יותר ככל שהאפליקציה הופכת למורכבת יותר.

זמן האחזור הכולל של מבט לאחור משפיע על מידת ההאזנות של בקרת הקצב (ועל אמצעי בקרה אחרים בזמן אמת). המרווח בין תזמון הקריאות הוא היגיון בין זמן האחזור המינימלי לבין התדירות שבה הקוד משפיע על המעבד. מידת החפיפה של המבט לאחור לשעת ההתחלה של המרווח הבא קובעת את מידת העמידות של האפליקציה במכונות שונות, ככל שהיא הופכת למורכבת יותר (והפריסה ואיסוף האשפה עשויים להימשך זמן רב יותר). באופן כללי, כדי לספק עמידות למכונות ולמערכות הפעלה איטיות יותר, מומלץ לשמור על מראה כללי רחב עם מרווח זמן קצר יחסית. אתם יכולים לבצע התאמות כדי ליהנות מחפיפות קצרות יותר וממרווחי זמן ארוכים יותר, כדי לעבד פחות קריאות חוזרות (callback). עם זאת, בשלב מסוים ייתכן שתתחילו לשמוע שזמן אחזור גדול גורם לשינויי קצב וכו', כך שהם לא ייכנסו לתוקף באופן מיידי. לחלופין, אם תפחיתו את המבט קדימה יותר מדי, ייתכן שתתחילו לשמוע רעידות (ייתכן ששיחה עם לוח זמנים "תמציא" אירועים שכבר היו אמורים להתרחש בעבר).

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

תזמון עם חפיפות ארוכות.
תזמון עם חפיפות ארוכות

למעשה, ניתן לראות בדוגמה הזו נתקלנו בהפרעה ל-setTimeout באמצע - הייתה אמורה להיות קריאה חוזרת מסוג setTimeout בערך ב-270 אלפיות השנייה, אבל היא התעכבה מסיבה כלשהי עד בערך 320 אלפיות השנייה - 50 אלפיות השנייה מאוחר יותר ממה שהיה אמור להיות! עם זאת, זמן האחזור הגדול של במבט קדימה שמר את התזמון בלי בעיה, ולא פספסנו פעימה, למרות שהגבנו את הקצב ממש לפני כן להשמעת תווים שש-עשרה בקצב של 240 פעימות לדקה (מעבר למקצבי התופים והבס!)

בנוסף, ייתכן שכל שיחה של מתזמן הבקשות תתזמן כמה הערות - נבחן עכשיו מה קורה כשנעשה שימוש במרווח זמן ארוך יותר (מבט קדימה של 250 אלפיות השנייה, מרווח של 200 אלפיות השנייה) והגברת הקצב באמצע.

setTimeout() עם מבט לאחור ארוך ומרווחים ארוכים.
setTimeout() עם מבט ארוך ומרווחים ארוכים

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

בפועל, מומלץ לכוונן את מרווח הזמן ואת המבט לאחור כדי לראות עד כמה זה מושפע מהפריסה, מאיסוף האשפה ומדברים אחרים שמתרחשים בשרשור הביצוע הראשי של JavaScript, ולכוונן את רמת הפירוט של השליטה בקצב וכו'. לדוגמה, אם יש לכם פריסה מורכבת מאוד שמתרחשת לעיתים קרובות, מומלץ להגדיל את המבט. הנקודה העיקרית היא שאנחנו רוצים שכמות "התזמון מראש" שאנחנו מתכננים תהיה גדולה מספיק כדי למנוע עיכובים, אבל לא כדי ליצור עיכוב מורגש כש לשנות את בקרת הקצב. גם במקרה שלמעלה יש חפיפה קטנה מאוד, כך שהוא לא עמיד מאוד במחשב איטי עם אפליקציית אינטרנט מורכבת. מומלץ להתחיל ב-100 אלפיות השנייה של זמן "מבט קדימה", עם מרווחי זמן של 25 אלפיות השנייה. זה עדיין עשוי לגרום לבעיות באפליקציות מורכבות עם זמן אחזור ארוך במערכת האודיו. במקרה כזה צריך לקצר את זמן החיפוש מראש. אם נדרשת שליטה הדוקה יותר על אובדן עמידות מסוימת, מומלץ להשתמש במבט קצר יותר.

קוד הליבה של תהליך התזמון נמצא בפונקציה Scheduler() –

while (nextNoteTime < audioContext.currentTime + scheduleAheadTime ) {
  scheduleNote( current16thNote, nextNoteTime );
  nextNote();
}

הפונקציה הזו רק מקבלת את הזמן הנוכחי של חומרת האודיו, ומשווה אותו לזמן של התו הבא ברצף – רוב הזמן* בתרחיש המדויק הזה לא יקרה דבר (מכיוון שאין 'הערות' של מטרונום שממתינות לתזמון, אבל כשהפעולה תצליח, היא תתזמן את ההערה באמצעות ממשק ה-API של Web Audio ויתקדם לתו הבא.

הפונקציה ScheduleNote() אחראית בפועל לתזמון ההפעלה של ה'הערה' הבאה באודיו באינטרנט. במקרה הזה, השתמשתי במתנדים כדי להשמיע צפצוף בתדרים שונים. באותה מידה ניתן ליצור צמתים של AudioBufferSource ולהגדיר את מאגרי האחסון שלהם לצלילי תופים או לכל צליל אחר שרוצים.

currentNoteStartTime = time;

// create an oscillator
var osc = audioContext.createOscillator();
osc.connect( audioContext.destination );

if (! (beatNumber % 16) )         // beat 0 == low pitch
  osc.frequency.value = 220.0;
else if (beatNumber % 4)          // quarter notes = medium pitch
  osc.frequency.value = 440.0;
else                              // other 16th notes = high pitch
  osc.frequency.value = 880.0;
osc.start( time );
osc.stop( time + noteLength );

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

שיטת nextNote() אחראית להתקדם לתו השישה-עשר הבא. כלומר, הגדרת המשתנים nextNoteTime ו-current16thNote עד לפתק הבא:

function nextNote() {
  // Advance current note and time by a 16th note...
  var secondsPerBeat = 60.0 / tempo;    // picks up the CURRENT tempo value!
  nextNoteTime += 0.25 * secondsPerBeat;    // Add 1/4 of quarter-note beat length to time

  current16thNote++;    // Advance the beat number, wrap to zero
  if (current16thNote == 16) {
    current16thNote = 0;
  }
}

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

טכניקת התזמון הזו נמצאת בשימוש בכמה יישומי אודיו אחרים באינטרנט – למשל, Web Audio Drum Machine, המשחק Acid Defender המהנה מאוד, ועוד דוגמאות אודיו מעמיקות יותר כמו הדגמה של אפקטים מפורטים.

עוד מערכת תזמון

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

למה, למה, אלוהים, למה אנחנו צריכים עוד מערכת תזמון? ובכן, התצוגה הזו מסונכרנת לתצוגה החזותית – כלומר, קצב הרענון של הגרפיקה – באמצעות requestAnimationFrame API. אולי זה לא ייראה כמו משהו גדול עבור תיבות ציור בדוגמה של המטרונום, אבל ככל שהגרפיקה הופכת למורכבת יותר ויותר, נעשה יותר ויותר קריטי להשתמש ב- requestAnimationFrame() כדי לסנכרן עם קצב הרענון החזותי - וזה קל מאוד לשימוש כבר מההתחלה כמו שימוש ב-setTimeout() ! עם גרפיקה מסונכרנת מורכבת מאוד (למשל, תצוגה מדויקת של תווים גרפיים צפופים (למשל, אנימציה מדויקת של התווים הגרפיים המבוקשים וחבילת AnimationFrame() תמושמע בחבילה גרפית מדויקת ולא מכוונת.

עקבנו אחרי הפעימות בתור במתזמן:

notesInQueue.push( { note: beatNumber, time: time } );

האינטראקציה עם השעה הנוכחית של המטרונום שלנו מופיעה בשיטת Paint() , שנקראת (באמצעות requestAnimationFrame) בכל פעם שמערכת הגרפיקה מוכנה לעדכון:

var currentTime = audioContext.currentTime;

while (notesInQueue.length && notesInQueue[0].time < currentTime) {
  currentNote = notesInQueue[0].note;
  notesInQueue.splice(0,1);   // remove note from queue
}

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

כמובן שיכולתי לדלג על כל שימוש בקריאה חוזרת (callback) מסוג setTimeout() ולהוסיף את מתזמן ההערות שלי בקריאה החוזרת של requestAnimationFrame. ואז נחזור שוב לשני טיימרים. זה בסדר לעשות את זה, אבל חשוב להבין ש- requestAnimationFrame היא רק הגדרה מובנית ל-setTimeout() במקרה הזה; עדיין תרצו לקבוע את דיוק התזמון של התזמון של Web Audio עבור ההערות עצמן.

סיכום

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