תזמון מדויק של אודיו באינטרנט
מבוא
אחד מהאתגרים הגדולים ביותר ביצירת תוכנות אודיו ומוזיקה מעולות באמצעות פלטפורמת האינטרנט הוא ניהול הזמן. לא כמו במקרה של "זמן לכתוב קוד", אלא כמו בשעות השעון – אחד מהנושאים שהכי פחות מובנים בינינו לגבי Web Audio הוא איך לעבוד בצורה נכונה עם שעון האודיו. לאובייקט AudioContext של Web Audio יש מאפיין currentTime שחשוף לשעון האודיו הזה.
במיוחד באפליקציות מוזיקה של אודיו באינטרנט – לא רק כתיבת סנסורים וסינתיסייזרים, אלא כל שימוש קצבתי באירועי אודיו כמו מכונות תופים, משחקים ואפליקציות אחרות – חשוב מאוד שהתזמון של אירועי האודיו יהיה עקבי ומדויק. לא רק הפעלה ועצירה של צלילים, אלא גם תזמון של שינויים בצלילים (כמו שינוי התדר או עוצמת הקול). לפעמים רוצים לנהל אירועים מעט אקראיים בזמן – לדוגמה, בהדגמה של נשק חם במאמר פיתוח Game Audio באמצעות Web Audio API. עם זאת, בדרך כלל חשוב לנו לקבוע תזמון עקבי ומדויק של תווים מוזיקליים.
כבר הראינו איך לתזמן תווים באמצעות הפרמטר הזמן של השיטות noteOn ו-noteOff (ששינו את השם ל-start ו-stop) ב-Web Audio, במאמר תחילת העבודה עם Web Audio וגם במאמר פיתוח אודיו למשחקים באמצעות Web Audio API. עם זאת, לא הרחבנו על תרחישים מורכבים יותר, כמו הפעלה של סדרות או קצב מוזיקלי ארוכים. כדי להתעמק בנושא הזה, אנחנו צריכים קודם קצת רקע בנושא שעונים.
The Best of Times – שעון האודיו באינטרנט
Web Audio API חושף גישה לשעון החומרה של מערכת האודיו המשנית. השעון הזה מוצג באובייקט AudioContext דרך המאפיין .currentTime שלו, כמספר של שניות בספרות עשרוניות מאז יצירת ה-AudioContext. כך השעון הזה (שנקרא מעכשיו 'שעון אודיו') יכול להיות מדויק מאוד. הוא תוכנן כך שיהיה אפשר לציין את ההתאמה ברמת דגימת הצליל הבודדת, גם עם קצב דגימה גבוה. מכיוון שיש בערך 15 ספרות עשרוניות של דיוק ב-double, גם אם שעון האודיו פועל במשך ימים, עדיין אמורים להישאר לו מספיק ביטים כדי להצביע על דגימה ספציפית, גם בקצב דגימה גבוה.
שעון האודיו משמש לתזמון פרמטרים ואירועי אודיו ב-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 המתוזמן מראש לפלט, רק כדי שהם יוכלו להשתיק את הצלילים שלהם בעצמם!)
בקיצור, מכיוון שתצטרכו גמישות כדי לשנות קצב או פרמטרים כמו תדירות או רווח (או להפסיק לגמרי את התזמון), לא תרצו לדחוף יותר מדי אירועי אודיו לתור. ליתר דיוק, לא תרצו להביט קדימה מדי בזמן, כי ייתכן שתרצו לשנות לגמרי את התזמון.
The Worst of Times – שעון JavaScript
יש לנו גם את שעון ה-JavaScript האהוב והשנוי במחלוקת, שמיוצג על ידי Date.now() ו-setTimeout(). הצד הטוב של שעון ה-JavaScript הוא שיש לו כמה שיטות שימושיות מאוד מסוג 'תתקשר אליי מאוחר יותר', window.setTimeout() ו-window.setInterval(), שמאפשרות לנו לגרום למערכת להתקשר אל הקוד שלנו בזמנים ספציפיים.
הצד הבעייתי של שעון JavaScript הוא שהוא לא מדויק במיוחד. קודם כול, הפונקציה Date.now() מחזירה ערך באלפיות שנייה – מספר שלם של אלפיות שנייה – כך שהדיוק הטוב ביותר שאפשר לקבל הוא אלפית שנייה אחת. המצב הזה לא גרוע במיוחד בהקשרים מוזיקליים מסוימים – אם התו הזה התחיל באלפיות השנייה מוקדם או מאוחר, אולי אפילו לא תבחינו בכך. אבל גם בקצב חומרת אודיו נמוך יחסית של 44.1kHz, השימוש שלו כשעון לתזמון אודיו איטי פי 44.1. חשוב לזכור שביטול של דגימות יכול לגרום לבעיות באודיו. לכן, אם אנחנו מחברים דגימות יחד, יכול להיות שנצטרך שהן יהיו בסדר מדויק.
המפרט החדש של High Resolution Time כן מספק לנו זמן נוכחי מדויק יותר באמצעות window.performance.now(); הוא מוטמע אפילו בדפדפנים רבים (אם כי עם קידומת) זה יכול לעזור במצבים מסוימים, אבל זה לא רלוונטי במיוחד לחלק הגרוע ביותר של ממשקי ה-API לניהול הזמן ב-JavaScript.
החלק הכי גרוע בממשקי ה-API לניהול הזמן ב-JavaScript הוא שגם אם הדיוק של Date.now() ברמת אלפית השנייה לא נשמע נורא, קל מאוד לשבש את הקריאה החוזרת בפועל של אירועי הטיימר ב-JavaScript (דרך window.setTimeout() או window.setInterval) בעשרות אלפיות שנייה או יותר בגלל פריסה, רינדור, איסוף אשפה, XMLHTTPRequest וקריאות חוזרות אחרות – בקיצור, בגלל כל מספר של דברים שמתרחשים בשרשור הביצוע הראשי. זוכרים שציינתי "אירועי אודיו" שאפשר לתזמן באמצעות Web Audio API? כולם מעובדים בשרשור נפרד. לכן, גם אם ה-thread הראשי נתקע זמנית בפריסה מורכבת או במשימה ארוכה אחרת, האודיו עדיין מופעל בדיוק ברגעים שבהם נאמר להם.
שימוש ב-JavaScript setTimeout() באפליקציות אודיו
מכיוון שה-thread הראשי יכול להיתקע בקלות למשך כמה אלפיות שנייה, לא כדאי להשתמש ב-setTimeout של JavaScript כדי להתחיל באופן ישיר להשמיע אירועי אודיו, כי במצב המיטבי, התווים יופעלו תוך אלפית שניות או כשהם אמורים להופיע, ובמקרה הגרוע ביותר הם יתעכבו עוד יותר. והכי גרוע, רצפים קצביים הם אלה שאמורים להיות קצביים, כי התזמון יהיה רגיש לדברים אחרים שמתרחשים בשרשור ה-JavaScript הראשי.
כדי להמחיש את זה, כתבתי אפליקציית מטרונום לדוגמה 'לא טובה' – כלומר אפליקציה שמשתמשת ב-setTimeout ישירות כדי לתזמן תווים – וגם מבצעת הרבה פריסה. פותחים את האפליקציה הזו, לוחצים על 'הפעלה' ואז משנים את גודל החלון במהירות בזמן ההפעלה. תוכלו לראות שהתזמון לא יציב (אפשר לשמוע שהקצב לא נשאר עקבי). "אבל זה מלאכותי!", אתם אומרים? כמובן – אבל זה לא אומר שזה לא קורה גם בעולם האמיתי. גם בממשק משתמש יחסית סטטי יהיו בעיות תזמון ב-setTimeout בגלל העברות חוזרות (relayouts) – לדוגמה, שמתי לב ששינוי מהיר של גודל החלון יגרום לגמגום ניכר בתזמון של WebkitSynth, שהוא מצוין בכל שאר הפרמטרים. עכשיו נסו לדמיין מה יקרה אם תנסו לגלול בצורה חלקה קטע מוזיקה מלא יחד עם האודיו. תוכלו לדמיין בקלות איך הפעולה הזו תשפיע על אפליקציות מוזיקה מורכבות בעולם האמיתי.
אחת מהשאלות הנפוצות ביותר שאני שומע היא "למה אי אפשר לקבל קריאות חזרה (callbacks) מאירועי אודיו?". יכול להיות שיש שימושים לסוגי הקריאות האלה, אבל הן לא יפתרו את הבעיה הספציפית הזו. חשוב להבין שהאירועים האלה יופעלו בשרשור הראשי של JavaScript, ולכן הם כפופים לאותם עיכובים פוטנציאליים כמו setTimeout. כלומר, יכול להיות שהם יתעכבו במספר לא ידוע ומשתנה של אלפיות השנייה מהשעה המדויקת שבה הם תוזמנו לפני שהם עובדו בפועל.
מה אפשר לעשות? ובכן, הדרך הטובה ביותר להתמודד עם התזמון היא להגדיר שיתוף פעולה בין טיימרים ב-JavaScript (setTimeout(), setInterval() או requestAnimationFrame() – מידע נוסף בנושא מאוחר יותר) לבין התזמון של חומרת האודיו.
איך מקבלים תזמון מדויק על ידי צפייה קדימה
נמשיך עם הדוגמה למטמון – למעשה, כתבתי את הגרסה הראשונה של הדוגמה הפשוטה הזו למטמון בצורה נכונה כדי להדגים את טכניקת התזמון המשותפת. (הקוד זמין גם ב-GitHub הדמו הזה מפעיל צפצופים (שמיוצרים על ידי מתנד) עם דיוק גבוה בכל שמינית, שמינית שנייה או שמינית רביעית, ומשנה את הצליל בהתאם לביט. אפשר גם לשנות את הקצב ואת מרווח התווים בזמן ההפעלה, או להפסיק את ההפעלה בכל שלב – זוהי תכונה חשובה בכל מכשיר לסנכרון קצב בעולם האמיתי. אפשר גם להוסיף קוד כדי לשנות את הצלילים שבהם המטמון הזה משתמש בזמן אמת.
הדרך שבה אנחנו מצליחים לאפשר בקרת טמפרטורה תוך שמירה על תזמון מדויק היא שיתוף פעולה: טיימר setTimeout שמופעל מדי פעם ומגדיר תזמון של Web Audio בעתיד עבור תווים ספציפיים. הטיימר setTimeout בעצם רק בודק אם צריך לתזמן תווים "בקרוב" על סמך הקצב הנוכחי, ואז מתזמן אותם, כך:
בפועל, קריאות setTimeout() עשויות להתעכב, כך שהתזמון של הקריאות לתזמון עשוי להיות רעידות (ויש הטיה, בהתאם לאופן שבו משתמשים ב-setTimeout) לאורך זמן. על אף שהאירועים בדוגמה הזו מופעלים בהפרש של כ-50 אלפיות השנייה, ברוב המקרים הם יהיו מעט גבוהים יותר מזה (ולפעמים הרבה יותר). עם זאת, במהלך כל שיחה, אנחנו מתזמנים אירועי Web Audio לא רק עבור הערות שצריך להשמיע עכשיו (למשל התו הראשון), אלא גם עבור כל התווים שצריך להשמיע מעכשיו ועד לפרק הזמן הבא.
למעשה, אנחנו לא רוצים לבחון רק את המרווח שבין קריאות setTimeout() . אנחנו זקוקים גם לתזמון מסוים של חפיפה בין הפעלת הטיימר הזו לפעולה הבאה, כדי לספק את ההתנהגות הגרועה ביותר בשרשור הראשי. כלומר, במקרה הגרוע ביותר של איסוף אשפה, פריסה, רינדור או קוד אחר שמתרחש ב-thread הראשי שמעכב את שיחת הטיימר הבאה שלנו. אנחנו צריכים גם להביא בחשבון את זמן תזמון הקצאת הבלוק של האודיו – כלומר, כמה אודיו מערכת ההפעלה שומרת במאגר העיבוד שלה – והוא משתנה בהתאם למערכות הפעלה ולחומרה, מספרים בודדים של אלפיות השנייה עד כ-50 אלפיות השנייה. לכל קריאה ל-setTimeout() שמוצגת למעלה יש מרווח זמן כחול שמציג את כל טווח הזמנים שבו היא תנסה לתזמן אירועים. לדוגמה, יכול להיות שהאירוע הרביעי של אודיו באינטרנט שתוזמן בתרשים שלמעלה יופעל "מאוחר" אם נמתין להפעלה שלו עד לקריאה הבאה ל-setTimeout, אם קריאה זו ל-setTimeout תתבצע רק כמה אלפיות השנייה מאוחר יותר. במציאות, התנודות בזמנים האלה יכולות להיות קיצוניות עוד יותר, והחפיפה הזו נעשית חשובה יותר ככל שהאפליקציה מורכבת יותר.
זמן האחזור הכולל של מבט לאחור משפיע על מידת ההשפעה של בקרת הקצב (ואמצעי בקרה אחרים בזמן אמת). המרווח בין תזמון שיחות מגשר בין זמן האחזור המינימלי לבין התדירות שבה הקוד שלך משפיע על המעבד. מידת החפיפה של זמן החזרה לאחור עם זמן ההתחלה של מרווח הזמן הבא קובעת את עמידות האפליקציה במכונות שונות, וככל שהיא נעשית מורכבת יותר (והפריסה ואיסוף האשפה עשויים להימשך זמן רב יותר). באופן כללי, כדי להתמודד עם מכונות ומערכות הפעלה איטיות יותר, מומלץ להגדיר זמן lookahead כולל גדול ומרחק זמן קצר יחסית. אפשר לשנות את ההגדרות כך שיהיה חפיפה קצרה יותר ומרווחי זמן ארוכים יותר, כדי לעבד פחות קריאות חוזרות (callbacks), אבל בשלב מסוים יכול להיות שתבחינו שזמן האחזור הארוך גורם לשינויים בקצב וכו' שלא נכנסים לתוקף באופן מיידי. לעומת זאת, אם תפחיתו את זמן החזרה העתידי (lookahead) יותר מדי, יכול להיות שתבחינו בתנודות מסוימות (jitter) (כי ייתכן שקריאה לתזמון תצטרך "ליצור" אירועים שהיו אמורים להתרחש בעבר).
תרשים התזמון הבא מראה מה קוד ההדגמה של המטרונום עושה בפועל: מרווח הזמן setTimeout של 25 אלפיות השנייה, אך חפיפה הרבה יותר עמידה: כל שיחה תתוזמן ל-100 האלפיות הבאות. החיסרון של המבט הארוך הזה הוא ששינויים בקצב וכו', ייכנסו לתוקף תוך עשירית שנייה. עם זאת, אנחנו עמידים הרבה יותר בפני הפרעות:
למעשה, אפשר לראות בדוגמה הזו שהיתה לנו הפרעה של setTimeout באמצע – אמורה הייתה להתבצע קריאה חוזרת (callback) של setTimeout בסביבות 270 אלפיות השנייה, אבל היא התעכבה מסיבה כלשהי עד לסביבות 320 אלפיות השנייה – 50 אלפיות השנייה מאוחר יותר מהזמן שהיה אמור להיות! עם זאת, זמן האחזור הארוך של תחזית העתיד אפשר לנו לשמור על התזמון בלי בעיה, ולא החמצנו אף פעימה, למרות שהגבשנו את הקצב ממש לפני כן כדי לנגן 16 שמיניות בקצב של 240 פעימות לדקה (מעבר לקצבים של טראקים של ד &ב!).
יכול להיות גם שכל קריאה במתזמן תתזמנ כמה הערות. בואו נסתכל על מה שקורה אם נשתמש במרווח זמן ארוך יותר (מבט לאחור של 250 אלפיות שנייה, מרווח של 200 אלפיות שנייה), ובאמצע עלייה בקצב:
הדוגמה הזו ממחישה שכל קריאה ל-setTimeout() עשויה בסופו של דבר לתזמן כמה אירועי אודיו – למעשה, המטמון הזה הוא אפליקציה פשוטה שמנגנת תו אחד בכל פעם, אבל אפשר לראות בקלות איך הגישה הזו פועלת במכונת תופים (שבה לעיתים קרובות יש כמה תווים בו-זמנית) או בסכימה (שבה לעיתים קרובות יש מרווחי זמן לא סדירים בין תווים).
בפועל, כדאי לשנות את מרווח הזמן לתזמון ואת זמן החזרה לאחור כדי לראות איך הוא מושפע מהפריסה, מאיסוף האשפה ומדברים אחרים שמתרחשים בשרשור הביצוע הראשי של JavaScript, וכדי לשנות את רמת הפירוט של הבקרה על הקצב וכו'. לדוגמה, אם יש לכם פריסה מורכבת מאוד שמתרחשת לעיתים קרובות, כדאי להגדיל את זמן החזרה לאחור. הנקודה העיקרית היא שאנחנו רוצים שכמות ה'תזמון מראש' שאנחנו עושים תהיה גדולה מספיק כדי למנוע עיכובים, אבל לא מספיק גדולה כדי למנוע עיכוב משמעותי כשמשנים את השליטה בקצב. גם למקרה שלמעלה יש חפיפה קטנה מאוד, כך שהוא לא יהיה עמיד במיוחד במכונה איטית עם אפליקציית אינטרנט מורכבת. מקום טוב להתחיל בו הוא זמן 'lookahead' של 100 אלפיות השנייה, עם מרווחים מוגדרים של 25 אלפיות השנייה. יכול להיות שיהיו בעיות באפליקציות מורכבות במכונות עם זמן אחזור ארוך של מערכת האודיו. במקרה כזה צריך להאריך את הזמן במבט-על. אם אתם זקוקים לשליטה קפדנית יותר במקרה של אובדן חוסן, תוכלו להשתמש במבט-על קצר יותר.
קוד הליבה של תהליך התזמון נמצא בפונקציה scheduler() –
while (nextNoteTime < audioContext.currentTime + scheduleAheadTime ) {
scheduleNote( current16thNote, nextNoteTime );
nextNote();
}
הפונקציה הזו פשוט מקבלת את השעה הנוכחית של חומרת האודיו ומשווה אותה לזמן של התו הבא ברצף. רוב הזמן* בתרחיש המדויק הזה לא יבצע כלום (מכיוון שאין 'הערות' של מטרונום בהמתנה לתזמון, אבל כשהוא מצליח, היא תתזמן את ההערה באמצעות ה-Web Audio API ותתקדם להערה הבאה.
הפונקציה scheduleNote() אחראית לתזמון בפועל של הנגינה של 'הצליל' הבא של Web Audio. במקרה הזה, השתמשתי באויללטורים כדי ליצור צלילים של צפצופים בתדרים שונים. אפשר ליצור בצורה דומה צמתים מסוג 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 ודוגמאות אודיו מעמיקות יותר כמו הדגמה של אפקטים גרגרי.
Yet Another Timing System
כמו שכל מוזיקאי טוב יודע, כל אפליקציית אודיו זקוקה ליותר פעמונים – או יותר שעונים. חשוב לציין שהדרך הנכונה להציג את התצוגה החזותית היא להשתמש במערכת זמנים שלישית!
למה, למה, למה אנחנו צריכים עוד מערכת זמנים? הוא מסתנכרן עם התצוגה החזותית – כלומר, קצב הרענון של הגרפיקה – באמצעות requestAnimationFrame API. כשמציירים תיבות בדוגמה שלנו למטמון, זה אולי לא נראה כזה חשוב, אבל ככל שהגרפיקה נעשית מורכבת יותר ויותר, כך חשוב יותר להשתמש ב-requestAnimationFrame() כדי לסנכרן עם קצב הרענון החזותי – ובאמת קל להשתמש בו מההתחלה כמו ב-setTimeout(). כשמדובר בגרפיקה מסונכרנת מורכבת מאוד (למשל, תצוגה מדויקת של תווים מוזיקליים צפופים בזמן שהם מושמעים בחבילת תווים מוזיקלית), ה-requestAnimationFrame() יספק את הסנכרון החלק והמדויק ביותר של אודיו וגרפיקה.
עקבנו אחרי הביטים בתור בלוח הזמנים:
notesInQueue.push( { note: beatNumber, time: time } );
האינטראקציה עם השעון הנוכחי של המטמטרון נמצאת בשיטה draw(), שנקראת (באמצעות 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() במקרה הזה. עדיין תרצו את הדיוק בתזמון של תזמון האודיו באינטרנט עבור ההערות בפועל.
סיכום
אני מקווה שהמדריך הזה עזר לכם להבין את הנושאים של שעונים, שעונים מעוררים ואיך ליצור תזמון מושלם באפליקציות אודיו לאינטרנט. אפשר להשתמש באותן שיטות כדי ליצור נגני רצף, מכונות תופים ועוד. להתראות בינתיים…