در تلفن اینترنتی، برابر نبودن فاصلهٔ ورود بسته‌ها می‌تواند بررسی مسیر صوت را پیچیده کند. اما عدد جیتر دقیقاً چه چیزی را نشان می‌دهد و با چه واحدی گزارش می‌شود؟ در این آموزش، هشت رکورد ساختگی می‌سازیم تا محاسبه، تبدیل واحد، عبور زمان از مرز شمارنده و ترتیب ورود را مستقل از تجهیزات واقعی آزمایش کنیم. هیچ تماس ضبط نشده و از نتیجه، کیفیت محصول یا دقت تشخیص گفتار نتیجه‌گیری نمی‌کنیم.

۱. دو زمان متفاوت برای یک بستهٔ صوت

تفکیک ساعت رسانه از زمان ورود بسته‌های RTP
تصویر مفهومی: تفکیک ساعت رسانه از زمان ورود بسته‌های RTP؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

برای هر رکورد، یک timestamp رسانه و یک زمان ورود در گیرنده داریم. در مثال، ساعت رسانه ۸۰۰۰ tick در ثانیه است و بسته‌های مبدأ با فاصلهٔ ۲۰ میلی‌ثانیه ساخته می‌شوند؛ در نتیجه فاصلهٔ timestampها ۱۶۰ tick است. جدول ۴ در RFC 3551 ساعت ۸۰۰۰ هرتز را برای نگاشت‌های PCMA و PCMU نشان می‌دهد. انتخاب بستهٔ ۲۰ میلی‌ثانیه‌ای در این تمرین، قانون عمومی همهٔ قالب‌های صوتی نیست.

زمان ورود را با میکروثانیهٔ صحیح ثبت می‌کنیم. تأخیرهای ساختگی به ترتیب ۵۰، ۵۱، ۴۹، ۵۵، ۵۲، ۵۳، ۵۰ و ۵۴ میلی‌ثانیه‌اند. چون زمان ارسال رکوردها در این شبیه‌سازی معلوم است، می‌توانیم اختلاف تأخیرها را برای مرجع مستقل حساب کنیم. در شبکهٔ واقعی، داشتن timestamp رسانه به تنهایی ساعت مبدأ و گیرنده را برای اندازه‌گیری مستقیم تأخیر یک‌طرفه هم‌زمان نمی‌کند.

در برنامه، تنها یک منبع با ساعت ثابت فرض شده است. تغییر نرخ ساعت، ترکیب منابع، استخراج سرآیند بسته و تشخیص نوع payload پیاده نشده‌اند. پیش از اتصال داده به مدل صوتی، نرخ رسانه را نیز از تنظیمات درست بگیرید؛ مقالهٔ تغییر نرخ نمونه‌برداری برای تلفن هوشمند به این مرز کمک می‌کند. نرخ فایل ورودی مدل و ساعت timestamp را نباید بدون بررسی قالب یکسان فرض کرد.

۲. اختلاف فاصله‌ها و به‌روزرسانی برآورد

سنجش اختلاف فاصلهٔ بسته‌ها در مبدأ و مقصد
تصویر مفهومی: سنجش اختلاف فاصلهٔ بسته‌ها در مبدأ و مقصد؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

طبق بخش ۶٫۴٫۱ در RFC 3550، ابتدا اختلاف فاصلهٔ ورود و فاصلهٔ رسانه را با واحد یکسان می‌گیریم و سپس قدرمطلق آن را با ضریب یک‌شانزدهم هموار می‌کنیم. رابطه در کد J += (abs(D) - J) / 16 است. رکوردها به ترتیب ورود بررسی می‌شوند؛ حتی اگر ترتیب timestampها متفاوت باشد. این کمیت را واریانس نمونه‌ای با واحد زمانِ مربع یا انحراف معیار معرفی نمی‌کنیم.

در گذار اول مثال، فاصلهٔ ورود ۲۱ میلی‌ثانیه و فاصلهٔ رسانه ۲۰ میلی‌ثانیه است. اختلاف یک میلی‌ثانیه با ساعت انتخاب‌شده برابر هشت tick می‌شود. با وضعیت اولیهٔ صفر، برآورد جدید نیم tick، یعنی ۰٫۰۶۲۵ میلی‌ثانیه است. این محاسبهٔ دستی، کنترل ساده‌ای است که می‌تواند خطای تبدیل میکروثانیه به ثانیه را پیش از تحلیل دادهٔ بزرگ آشکار کند.

مبدأ عددی ساعت‌ها در مثال اهمیت ندارد؛ فقط اختلاف رکوردهای مجاور مصرف می‌شود. آزمون، تمام زمان‌های ورود را به یک اندازه جابه‌جا می‌کند و انتظار دارد نتیجه ثابت بماند. برای ساعت رسانه نیز شروع معمولی را با شروع نزدیک مرز شمارنده مقایسه می‌کنیم. این دو کنترل، به‌تنهایی وجود رانش ساعت یا درستی زمان‌سنجی سیستم‌عامل در یک دستگاه واقعی را تأیید نمی‌کنند.

۳. کد کامل با مرجع مستقل و دوازده آزمون

آزمون محاسبهٔ جیتر و کنترل واحدهای برنامه
تصویر مفهومی: آزمون محاسبهٔ جیتر و کنترل واحدهای برنامه؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

کد زیر فقط کتابخانهٔ استاندارد می‌خواهد و با پایتون ۳٫۱۴٫۵ اجرا شده است. آن را در فایل rtp_jitter.py ذخیره و با python rtp_jitter.py اجرا کنید. از Fraction برای نگه داشتن سهم‌های کسری استفاده کرده‌ایم؛ این انتخاب آموزشی، خروجی صحیحِ بستهٔ RTCP را روی شبکه تولید نمی‌کند. تبدیل مقدار داخلی به فیلد عدد صحیح گزارش، مرحلهٔ جداگانه‌ای است.

"""Synthetic fixed-clock RTP timestamp fixture; no network traffic captured."""
from fractions import Fraction
import json
import platform

MODULUS = 2**32
DELAYS_US = [50000, 51000, 49000, 55000, 52000, 53000, 50000, 54000]


def signed_timestamp_delta(current, previous):
    delta = (current - previous + MODULUS // 2) % MODULUS - MODULUS // 2
    if delta == -MODULUS // 2:
        raise ValueError("Half-cycle timestamp difference is ambiguous")
    return delta


def make_fixture(delays=DELAYS_US, start=MODULUS - 160):
    return [(int((start + 160*i) % MODULUS), 20000*i + delay)
            for i, delay in enumerate(delays)]


def estimate(packets, clock_hz=8000):
    if type(clock_hz) is not int or clock_hz <= 0 or len(packets) < 2:
        raise ValueError("Positive integer clock and at least two packets required")
    previous, jitter, trace = None, Fraction(0), []
    for timestamp, arrival_us in packets:
        if type(timestamp) is not int or not 0 <= timestamp < MODULUS:
            raise ValueError("Timestamp must be an unsigned32-bit integer")
        if type(arrival_us) is not int or arrival_us < 0:
            raise ValueError("Arrival time must be nonnegative integer microseconds")
        difference = Fraction(0)
        if previous is not None:
            old_timestamp, old_arrival = previous
            if arrival_us < old_arrival:
                raise ValueError("Keep records in nondecreasing arrival order")
            send_delta = signed_timestamp_delta(timestamp, old_timestamp)
            receive_delta = Fraction((arrival_us-old_arrival)*clock_hz, 1_000_000)
            difference = receive_delta - send_delta
            jitter += (abs(difference)-jitter)/16
        trace.append({"timestamp": timestamp, "arrival_us": arrival_us,
                      "difference_ticks": float(difference),
                      "jitter_ticks": float(jitter),
                      "jitter_ms": float(jitter*1000/clock_hz)})
        previous = timestamp, arrival_us
    return {"clock_hz": clock_hz, "jitter_ticks_exact": str(jitter),
            "jitter_ms": float(jitter*1000/clock_hz), "trace": trace}


def rejects(call):
    try:
        call()
    except ValueError:
        return True
    return False


def run_example():
    packets = make_fixture()
    result = estimate(packets)
    delay_differences = [abs(b-a) for a, b in zip(DELAYS_US, DELAYS_US[1:])]
    # Independent closed-form weighted sum in microseconds, using known synthetic delays.
    reference_us = sum(Fraction(d, 16)*Fraction(15, 16)**(6-i)
                       for i, d in enumerate(delay_differences))
    shifted_arrivals = [(t, a+123000) for t, a in packets]
    reordered = estimate([(0, 50000), (320, 90000), (160, 91000)])
    checks = {
        "wrap_delta_preserved": signed_timestamp_delta(0, MODULUS-160) == 160,
        "negative_delta_for_reordered_timestamp": signed_timestamp_delta(160, 320) == -160,
        "first_record_initializes_zero": result["trace"][0]["jitter_ticks"] == 0,
        "first_update_is_half_tick": result["trace"][1]["jitter_ticks"] == 0.5,
        "constant_delay_zero_jitter": estimate(make_fixture([50000]*8))["jitter_ms"] == 0,
        "closed_form_reference_matches": Fraction(result["jitter_ticks_exact"])*125 == reference_us,
        "timestamp_origin_invariant": estimate(make_fixture(start=4000))["jitter_ticks_exact"] == result["jitter_ticks_exact"],
        "arrival_origin_invariant": estimate(shifted_arrivals)["jitter_ticks_exact"] == result["jitter_ticks_exact"],
        "reordering_uses_arrival_order": reordered["jitter_ms"] == 1.3125,
        "invalid_clock_rejected": rejects(lambda: estimate(packets, 0)),
        "arrival_order_violation_rejected": rejects(lambda: estimate(list(reversed(packets)))),
        "ambiguous_half_cycle_rejected": rejects(lambda: signed_timestamp_delta(MODULUS//2, 0)),
    }
    assert all(checks.values()), checks
    return {"python": platform.python_version(), "synthetic_only": True,
            "packet_interval_ms": 20, "result": result,
            "known_delay_differences_us": delay_differences,
            "reordered_fixture_jitter_ms": reordered["jitter_ms"], "checks": checks}


if __name__ == "__main__":
    print(json.dumps(run_example(), indent=2))

مرجع مستقل از فهرست تأخیرهای معلوم ساخته می‌شود: اختلاف‌های قدرمطلق را با وزن‌های یک‌شانزدهم و توان‌های پانزده‌شانزدهم جمع می‌کنیم. این روش، همان حلقهٔ برنامه را دوباره اجرا نمی‌کند و برای همین سناریوی هفت گذار یک پاسخ بسته می‌دهد. سپس مقدار کسری آن با خروجی حلقه مقایسه می‌شود. مستقل بودن ورودی مرجع از timestampهای دورخورده، کشف خطای حساب پیمانه‌ای را آسان‌تر می‌کند.

دوازده بررسی شامل عبور مرز، اختلاف منفی timestamp، آغاز صفر، نخستین به‌روزرسانی، تأخیر ثابت، مرجع بسته، جابه‌جایی مبدأ دو ساعت، بازترتیب، ساعت نامعتبر، ترتیب نامعتبر زمان ورود و مرز مبهم نیم‌چرخه است. این پوشش برای تمرین محدود ماست؛ گیرندهٔ RTP کامل به مدیریت منبع و بسته‌های تکراری یا گم‌شده هم نیاز دارد. تابع، نرخ فقدان بسته را محاسبه نمی‌کند.

۴. خروجی ثبت‌شده، سرریز و بازترتیب

عبور شمارندهٔ زمان رسانه از مرز چرخه
تصویر مفهومی: عبور شمارندهٔ زمان رسانه از مرز چرخه؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

برآورد نهایی این هشت رکورد حدود ۸٫۴۶۲۷۵۷ tick، برابر ۱٫۰۵۷۸۴۵ میلی‌ثانیه است. اختلاف‌های تأخیر معلوم، یک، دو، شش، سه، یک، سه و چهار میلی‌ثانیه‌اند؛ برآورد هموارشده نه بیشینهٔ این فهرست است و نه میانگین سادهٔ آن. برای بازبینی، گزارش هر گام و مقدار کسری نهایی در خروجی حفظ شده است. اندازهٔ کوچک این مثال برای اعلام خوب یا بد بودن شبکه کافی نیست.

{
  "python": "3.14.5",
  "synthetic_only": true,
  "packet_interval_ms": 20,
  "result": {
    "clock_hz": 8000,
    "jitter_ticks_exact": "283963009/33554432",
    "jitter_ms": 1.0578446425497532,
    "trace": [
      {
        "timestamp": 4294967136,
        "arrival_us": 50000,
        "difference_ticks": 0.0,
        "jitter_ticks": 0.0,
        "jitter_ms": 0.0
      },
      {
        "timestamp": 0,
        "arrival_us": 71000,
        "difference_ticks": 8.0,
        "jitter_ticks": 0.5,
        "jitter_ms": 0.0625
      },
      {
        "timestamp": 160,
        "arrival_us": 89000,
        "difference_ticks": -16.0,
        "jitter_ticks": 1.46875,
        "jitter_ms": 0.18359375
      },
      {
        "timestamp": 320,
        "arrival_us": 115000,
        "difference_ticks": 48.0,
        "jitter_ticks": 4.376953125,
        "jitter_ms": 0.547119140625
      },
      {
        "timestamp": 480,
        "arrival_us": 132000,
        "difference_ticks": -24.0,
        "jitter_ticks": 5.6033935546875,
        "jitter_ms": 0.7004241943359375
      },
      {
        "timestamp": 640,
        "arrival_us": 153000,
        "difference_ticks": 8.0,
        "jitter_ticks": 5.753181457519531,
        "jitter_ms": 0.7191476821899414
      },
      {
        "timestamp": 800,
        "arrival_us": 170000,
        "difference_ticks": -24.0,
        "jitter_ticks": 6.8936076164245605,
        "jitter_ms": 0.8617009520530701
      },
      {
        "timestamp": 960,
        "arrival_us": 194000,
        "difference_ticks": 32.0,
        "jitter_ticks": 8.462757140398026,
        "jitter_ms": 1.0578446425497532
      }
    ]
  },
  "known_delay_differences_us": [
    1000,
    2000,
    6000,
    3000,
    1000,
    3000,
    4000
  ],
  "reordered_fixture_jitter_ms": 1.3125,
  "checks": {
    "wrap_delta_preserved": true,
    "negative_delta_for_reordered_timestamp": true,
    "first_record_initializes_zero": true,
    "first_update_is_half_tick": true,
    "constant_delay_zero_jitter": true,
    "closed_form_reference_matches": true,
    "timestamp_origin_invariant": true,
    "arrival_origin_invariant": true,
    "reordering_uses_arrival_order": true,
    "invalid_clock_rejected": true,
    "arrival_order_violation_rejected": true,
    "ambiguous_half_cycle_rejected": true
  }
}

timestamp اول ۴۲۹۴۹۶۷۱۳۶ و بعدی صفر است. تفاضل سادهٔ عددهای بدون علامت، فاصله‌ای بسیار بزرگ و منفی می‌دهد؛ روش پیمانه‌ای این کد فاصلهٔ ۱۶۰ tick را نگه می‌دارد. فرض ضروری آن است که اختلاف واقعی مجاور کمتر از نیم چرخه باشد. دقیقاً نیم چرخه مبهم است و رد می‌شود؛ وقفه‌های طولانی یا تغییر مبدأ واقعی منبع، با این ترفند به‌تنهایی حل نمی‌شوند.

در آزمون بازترتیب، timestampها صفر، ۳۲۰ و ۱۶۰ هستند، اما زمان‌های ورود ۵۰، ۹۰ و ۹۱ میلی‌ثانیه افزایش می‌یابند. خروجی این سناریو ۱٫۳۱۲۵ میلی‌ثانیه است. مرتب کردن بر اساس timestamp، ترتیب مشاهده را عوض می‌کند؛ به همین علت برنامه ورودی را بازمرتب نمی‌کند. کاهش زمان ورود نیز خطا می‌دهد، زیرا با قرارداد ثبت زمان این مثال سازگار نیست. شمارندهٔ توالی بسته در این تمرین ورودی تابع نیست.

۵. پاسخ تمرین‌ها و تفاوت جیتر با تأخیر بافر

تفاوت اندازه‌گیری جیتر و نگهداری صوت در بافر
تصویر مفهومی: تفاوت اندازه‌گیری جیتر و نگهداری صوت در بافر؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

تمرین اول: اگر تأخیر تمام رکوردها ۵۰ میلی‌ثانیه باشد چه می‌شود؟ پاسخ: برآورد از صفر شروع می‌شود و صفر می‌ماند، با اینکه خود تأخیر صفر نیست. تمرین دوم: چرا تقسیم tick بر ۸۰۰۰ لازم است؟ پاسخ: هر ثانیه ۸۰۰۰ tick دارد؛ پس برای ثانیه تقسیم بر ۸۰۰۰ و برای میلی‌ثانیه ضرب نتیجه در هزار لازم است. این نرخ باید متعلق به همان منبع باشد.

تمرین سوم: آیا عدد نهایی، اندازهٔ پیشنهادی بافر است؟ پاسخ: خیر. در پیش‌نویس آمار WebRTC منتشرشده در ۲۵ سپتامبر ۲۰۲۵، jitter با واحد ثانیه و شاخص‌های بافر جدا تعریف شده‌اند. میانگین تأخیر بافر از تقسیم جمع تأخیر بر تعداد واحدهای خارج‌شده به دست می‌آید؛ در صوت، واحد شمارش نمونه است و لزوماً بسته نیست. این سند هنوز Candidate Recommendation Draft است.

تمرین چهارم: آیا جیتر کم، دقت خوب تشخیص گفتار را ثابت می‌کند؟ پاسخ: خیر؛ این تمرین نه صوت را رمزگشایی کرده و نه مدل را آزموده است. تحلیل طیف صوت تلفن و پنجره‌بندی نیز فقط بخش دیگری از بررسی زنجیره است. همهٔ منابع در ۲۳ مهر ۱۴۰۵ خوانده شده‌اند؛ نتایج حاضر آزمایش عددی تألیفی هستند و گزارش تماس واقعی یا خبر پژوهشی تازه محسوب نمی‌شوند.

منابع و مطالعهٔ بیشتر

بررسی منابع: ۲۳ مهر ۱۴۰۵. تحلیل‌ها و پیشنهادهای اجرایی، جمع‌بندی نویسنده‌اند.