Skip to content

שמע רב־משתתפים בדפדפן: WebRTC, אומדן גובה צליל ומגבלות תזמון

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

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

נבחן תכנון להמחשה של שמע רב־משתתפים ואת הפשרות בין WebSockets,‏ WebRTC וניתוח מקומי ב־Web Audio. לא הורץ כאן מימוש ולא נערכו מדידות ביצועים.

האתגר המרכזי: הקרב בהשהיה

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

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

  • המסלול האקוסטי: התפשטות הקול תלויה במרחק ובסביבה; אין ערך אוניברסלי של 10–30 מילישניות.
  • לכידה והשמעה: מאגרי המכשיר, תזמון מערכת ההפעלה ועיבוד הדפדפן מוסיפים השהיה; השהיה מבוקשת אינה השהיה שנמדדה.
  • רשת וניגון: השהיית מסלול, מאגר ריצוד, מאגר קודק והתאוששות מאובדן משתנים לפי התנאים. מדדו בנפרד השהיה חד־כיוונית והלוך־ושוב.

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

הארכיטקטורה: WebRTC לעומת WebSockets

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

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

אפשרות אחת היא להפריד בין תעבורת המדיה לבקרת המשחק:

לקוח A / לקוח B: לכידה וניתוח Web Audio מקומי
מדיה: חיבור עמיתים WebRTC, ישירות או דרך SFU
TURN: ממסר קישוריות אפשרי, שאינו SFU
בקרה ואיתות: שרת היישום, למשל דרך WebSocket
בחירת התעבורה וההשהיה תלויות בקישוריות ובמשא ומתן.

לפי הפרדת תחומי אחריות:

  • ערוץ מצב המשחק — WebSockets או WebTransport: מטפל ביצירת חדר, הצטרפות שחקנים, צ׳אט, סנכרון מילות השיר והפצת הניקוד הסופי.
  • ערוץ השמע: WebRTC מעביר מדיה מקודדת שנקבעה במשא ומתן ובתעבורה מאובטחת, לא בתי מיקרופון גולמיים דרך socket UDP של JavaScript. ‏SFU מעביר מדיה; TURN מתווך תעבורת קישוריות.

שלב אחר שלב: לכידת שמע ועיבודו בצד הלקוח

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

1. גישה למיקרופון והקמת ההקשר

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

let activeAudio = null;
// Wire these functions to explicit Start and Stop controls.
async function disposeAudio(session) {
  session.cancelled = true;
  session.stopAnalysis?.();
  session.stream?.getTracks().forEach(track => track.stop());
  if (session.context.state !== "closed") {
    try { await session.context.close(); } catch (_) { /* already closing */ }
  }
}
async function initAudio() {
  if (activeAudio !== null) return; // Ignore duplicate Start.
  const session = {
    context: new AudioContext({ latencyHint: "interactive" }),
    stream: null, stopAnalysis: null, cancelled: false
  };
  activeAudio = session;
  try {
    await session.context.resume();
    if (session.cancelled) return;
    const stream = await navigator.mediaDevices.getUserMedia({
      audio: { echoCancellation: false,
        noiseSuppression: false, autoGainControl: false }
    });
    // Stop may have occurred while the permission prompt was pending.
    if (session.cancelled || activeAudio !== session) {
      stream.getTracks().forEach(track => track.stop());
      return;
    }
    session.stream = stream;
    const source = session.context.createMediaStreamSource(stream);
    session.stopAnalysis = setupAudioPipeline(session.context, source);
  } catch (err) {
    if (activeAudio === session) {
      activeAudio = null;
      console.error("Audio could not start:", err.name);
    }
    await disposeAudio(session);
  }
}
async function stopAudio() {
  const session = activeAudio;
  activeAudio = null; // Any pending Start is now stale.
  if (session !== null) await disposeAudio(session);
}

2. צינור Web Audio וזיהוי גובה הצליל

AnalyserNode חושף דגימות לאומדן גובה צליל להמחשה. אוטוקורלציה או YIN דורשים חלונות מתאימים, טיפול ברמת ביטחון ואימות; אומדן גובה צליל לבדו אינו ציון הוגן לשירה.

function setupAudioPipeline(context, sourceNode) {
  const analyser = context.createAnalyser();
  analyser.fftSize = 2048;
  sourceNode.connect(analyser);
  // No connection to speakers: do not monitor the microphone here.
  const samples = new Float32Array(analyser.fftSize);
  let frame;
  function analyze() {
    analyser.getFloatTimeDomainData(samples);
    const pitch = autoCorrelate(samples, context.sampleRate);
    // Expose an estimate to a UI; this is not a scoring algorithm.
    // requestAnimationFrame can be throttled in background tabs.
    frame = requestAnimationFrame(analyze);
  }
  analyze();
  return () => { cancelAnimationFrame(frame); sourceNode.disconnect(); };
}

3. מימוש אוטוקורלציה בסיסית

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

function autoCorrelate(buffer, sampleRate) {
  if (buffer.length < 32 || !(sampleRate > 0)) return -1;
  const mean = buffer.reduce((a, b) => a + b, 0) / buffer.length;
  const x = Float64Array.from(buffer, value => value - mean);
  const energy = x.reduce((a, b) => a + b * b, 0);
  if (Math.sqrt(energy / x.length) < 0.01) return -1;
  const first = Math.max(1, Math.ceil(sampleRate / 1000));
  const last = Math.min(Math.floor(sampleRate / 80),
                        Math.floor(x.length / 2) - 1);
  const scores = new Float64Array(last + 2);
  for (let lag = first - 1; lag <= last + 1; lag++) {
    let xy = 0, xx = 0, yy = 0;
    for (let i = 0; i < x.length - lag; i++) {
      const a = x[i], b = x[i + lag];
      xy += a * b; xx += a * a; yy += b * b;
    }
    scores[lag] = xy / Math.sqrt(xx * yy || 1);
  }
  let bestLag = -1, best = 0.7;
  for (let lag = first; lag <= last; lag++) {
    if (scores[lag] > best && scores[lag] >= scores[lag - 1]
        && scores[lag] >= scores[lag + 1]) {
      best = scores[lag]; bestLag = lag;
    }
  }
  return bestLag < 0 ? -1 : sampleRate / bestLag;
}

סנכרון החדר

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

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

  1. הלקוח שולח לשרת ping עם חותמת הזמן הנוכחית שלו ($T_1$).
  2. השרת מקבל את ה־ping, רושם חותמת זמן מצדו ($T_2$) ומחזיר מיד pong עם $T_1$ ו־$T_2$, וכן חותמת זמן השליחה של השרת ($T_3$).
  3. הלקוח מקבל את ה־pong בחותמת הזמן המקומית ($T_4$).
  4. כעת הלקוח יכול לחשב את זמן ההלוך־ושוב (RTT) ואת הפרש השעון יחסית לשרת:
// All timestamps use milliseconds; each pair uses the same local clock.
const rttEstimate = (t4 - t1) - (t3 - t2);
const offsetEstimate = ((t2 - t1) + (t3 - t4)) / 2;
// offsetEstimate means server clock minus client clock.
const estimatedServerTime = clientNow() + offsetEstimate;
// clientNow() must use the same time base as t1 and t4.

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

סיכום: הרשת היא כעת מגרש משחקים בזמן אמת

Web Audio,‏ WebRTC וערוץ בקרה יישומי יכולים לתמוך בחוויות שמע אינטראקטיביות. התכנון עדיין דורש הסכמה, הקמת תעבורה, בחירת קודק, נגישות ומדידות תזמון במכשירי היעד; הדוגמאות אינן מוכיחות ארכיטקטורה או ביצועים של מוצר כלשהו.

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

האם התנסיתם ב־WebAudio או WebRTC במיזמים שלכם? באילו אסטרטגיות אתם נלחמים בריצוד רשת ובהשהיית שמע? כתבו בתגובות, או שתפו את המאמר עם צוות הפיתוח שלכם!

עד הפעם הבאה, המשיכו לתכנת.

מקורות ראשוניים

Post a Comment

Previous Post Next Post