۱. سیگنال دوفرکانسی و قرارداد آزمایش را مشخص کنید

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

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

در صفحه‌کلید دوازده‌کلیدی، هر سطر و ستون یک فرکانس دارد. مطابق توضیح فنی MathWorks دربارهٔ DTMF، گروه پایین شامل ۶۹۷، ۷۷۰، ۸۵۲ و ۹۴۱ هرتز و گروه بالا شامل ۱۲۰۹، ۱۳۳۶ و ۱۴۷۷ هرتز است. بنابراین کلید ۵ از جفت ۷۷۰ و ۱۳۳۶ هرتز تشکیل می‌شود. این مثال سه ستون معمول را پوشش می‌دهد؛ ستون چهارم کلیدهای A تا D در آن پیاده‌سازی نشده است.

نرخ نمونه‌برداری را ۸۰۰۰ هرتز و طول قاب را ۸۰۰ نمونه انتخاب می‌کنیم؛ مدت مشاهده برابر ۱۰۰ میلی‌ثانیه است. تابع ورودی فقط آرایهٔ حقیقی، متناهی، تک‌کاناله و دقیقاً با همین طول را می‌پذیرد. نرخ نمونه‌برداری در کد ثابت است و در یک آرایهٔ خام ذخیره نمی‌شود؛ کنترل آن بر عهدهٔ فراخواننده است. فایل ۱۶ کیلوهرتزی با ۸۰۰ نمونه همین قرارداد را ندارد. تبدیل نرخ، تشخیص آغاز تن و جداسازی چند کلید پشت سر هم، موضوع این نسخه نیستند.

۲. انرژی هفت فرکانس را با Goertzel محاسبه کنید

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

به جای محاسبهٔ کل طیف، مقدار تبدیل را در هفت فرکانس هدف می‌خواهیم. برای هر فرکانس، یک بازگشت مرتبهٔ دوم با ضریب دو برابر کسینوس فرکانس نرمال‌شده اجرا می‌شود. دو حالت داخلی در ابتدا صفرند و پس از آخرین نمونه، از آن‌ها مربع اندازهٔ تبدیل به دست می‌آید. مرجع Goertzel در MathWorks ارزیابی تبدیل در فرکانس‌های غیرمنطبق با خانه‌های صحیح DFT را نیز توضیح می‌دهد. در پیاده‌سازی ما فرکانس هدف گرد نمی‌شود؛ مقایسهٔ مستقل با جمع مستقیم مختلط، در بخش بعد خطای عددی را کنترل می‌کند.

omega = 2*pi*f/fs
c = 2*cos(omega)
s[n] = x[n] + c*s[n-1] - s[n-2]
power = s_last**2 + s_previous**2 - c*s_last*s_previous

پیش از تحلیل، قاب در پنجرهٔ Hann متقارن ضرب می‌شود؛ تعریف دقیق آن در مستندات numpy.hanning آمده است. این انتخاب اثر مرزهای قاب را تغییر می‌دهد، اما جداسازی کامل همهٔ سیگنال‌های مزاحم را تضمین نمی‌کند. برای برآورد دامنه، دو برابر ریشهٔ انرژی را بر مجموع ضرایب پنجره تقسیم می‌کنیم. این تقریب در حضور چند مؤلفه، فاصلهٔ فرکانسی محدود و نویز خطا دارد؛ مقدار حاصل را نباید چگالی توان طیفی یا اندازه‌گیری کالیبرهٔ ولتاژ دانست.

در هر گروه، بزرگ‌ترین دامنه انتخاب می‌شود. پذیرش علاوه بر آن به چهار شرط آموزشی وابسته است: RMS حداقل ۰٫۰۳، دامنهٔ هر دو مؤلفه حداقل ۰٫۱، برتری هر برنده نسبت به رقیب همان گروه حداقل ۶ دسی‌بل و اختلاف دامنهٔ دو برنده حداکثر ۶ دسی‌بل. این اعداد را برای تمرین انتخاب کرده‌ایم؛ حدود پذیرش استاندارد مخابراتی نیستند. دامنه‌ها واحد نمونهٔ نرمال‌شده دارند و به dBm0 تبدیل نشده‌اند.

۳. کد کامل، دادهٔ مصنوعی و آزمون مستقل

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

برنامهٔ زیر فقط به NumPy نیاز دارد. برای هر تن دامنهٔ ۰٫۳ و دو فاز متفاوت قرار داده‌ایم؛ نویز با مولد دارای بذر ۱۷ ساخته می‌شود. این کار نمونهٔ آزمایش را تکرارپذیر می‌کند، اما تنوع شبکه و گفتار واقعی را بازنمایی نمی‌کند. برای تطبیق الگوریتم، همان قاب پنجره‌خورده را یک‌بار با بازگشت و بار دیگر با جمع مستقیم نمایی مختلط تحلیل می‌کنیم. مقایسه با ورودی یا پنجرهٔ متفاوت، آزمون معتبری برای این رابطه نخواهد بود.

نسخهٔ اجراشده Python 3.14.5 و NumPy 2.5.4 است. فایل را با نام dtmf_goertzel.py ذخیره و با دستور python dtmf_goertzel.py اجرا کنید. برنامه در پایان گزارش JSON، نسخه‌ها، شناسهٔ محتوایی نمونه و نتیجهٔ بررسی‌ها را چاپ می‌کند. توابع detect و tone از هم جدا هستند تا بعداً بتوان مولد آزمون را بدون تغییر تصمیم‌گیر عوض کرد. نمونهٔ MATLAB در منابع صرفاً برای مطالعه است؛ اجرای MATLAB برای این آموزش انجام نشده است.

"""Authored offline DTMF teaching fixture, not a compliant telecom receiver."""
import hashlib
import json
import platform
import numpy as np

FS = 8000
N = 800  # exactly 100 ms; no streaming segmentation is implemented
LOW = np.array([697., 770., 852., 941.])
HIGH = np.array([1209., 1336., 1477.])
FREQS = np.r_[LOW, HIGH]
KEYS = np.array([list('123'), list('456'), list('789'), list('*0#')])


def validate_frame(frame):
    a = np.asarray(frame)
    if a.ndim != 1 or a.size != N or np.iscomplexobj(a):
        raise ValueError('Expected 800 real mono samples at 8000 Hz')
    a = a.astype(np.float64)
    if not np.all(np.isfinite(a)):
        raise ValueError('Samples must be finite')
    return a


def goertzel_power(frame, freqs=FREQS):
    """Squared DTFT magnitudes, evaluated at the given exact frequencies."""
    c = 2 * np.cos(2 * np.pi * np.asarray(freqs) / FS)
    s1, s2 = np.zeros_like(c), np.zeros_like(c)
    for sample in frame:
        s0 = sample + c * s1 - s2
        s2, s1 = s1, s0
    return np.maximum(s1 * s1 + s2 * s2 - c * s1 * s2, 0.)


def tone(key, noise_std=0., seed=17):
    row, col = np.argwhere(KEYS == key)[0]
    t = np.arange(N) / FS
    x = .3 * np.sin(2 * np.pi * LOW[row] * t + .23)
    x += .3 * np.sin(2 * np.pi * HIGH[col] * t + .71)
    return x + np.random.default_rng(seed).normal(0, noise_std, N)


def detect(frame):
    x = validate_frame(frame)
    rms = float(np.sqrt(np.mean(x * x)))
    w = np.hanning(N)  # symmetric Hann; explicit and identical in cross-check
    amplitudes = 2 * np.sqrt(goertzel_power(x * w)) / np.sum(w)
    row, col = int(np.argmax(amplitudes[:4])), int(np.argmax(amplitudes[4:]))
    lo, hi = amplitudes[row], amplitudes[4 + col]
    eps = np.finfo(float).tiny
    competitors = [np.max(np.delete(amplitudes[:4], row)),
                   np.max(np.delete(amplitudes[4:], col))]
    margins = [20 * np.log10(max(lo, eps) / max(competitors[0], eps)),
               20 * np.log10(max(hi, eps) / max(competitors[1], eps))]
    balance = float(20 * np.log10(max(hi, eps) / max(lo, eps)))
    # These thresholds are authored teaching choices, not ITU acceptance limits.
    accepted = (rms >= .03 and min(lo, hi) >= .1
                and min(margins) >= 6. and abs(balance) <= 6.)
    return {'key': str(KEYS[row, col]) if accepted else None,
            'rms': rms, 'low_hz': float(LOW[row]), 'high_hz': float(HIGH[col]),
            'amplitudes': amplitudes.tolist(), 'margin_db': margins,
            'balance_db': balance}


def main():
    checks = {}
    x = tone('5')
    z = x * np.hanning(N)
    n = np.arange(N)
    direct = np.exp(-2j * np.pi * FREQS[:, None] * n / FS) @ z
    gp = goertzel_power(z)
    np.testing.assert_allclose(gp, np.abs(direct) ** 2, rtol=1e-9, atol=1e-9)
    checks['recurrence_matches_direct_dtft'] = True
    keys = KEYS.ravel().tolist()
    assert all(detect(tone(k))['key'] == k for k in keys)
    checks['all_12_clean_authored_keys'] = True
    assert all(detect(tone(k, .02))['key'] == k for k in keys)
    checks['all_12_fixed_noise_fixtures'] = True
    assert detect(np.zeros(N))['key'] is None
    checks['silence_rejected'] = True
    single = .3 * np.sin(2 * np.pi * 770 * n / FS)
    assert detect(single)['key'] is None
    checks['single_tone_rejected'] = True
    assert detect(.1 * x)['key'] is None
    checks['quiet_fixture_rejected_by_authored_threshold'] = True
    assert detect(-x)['key'] == detect(x)['key'] == '5'
    checks['polarity_invariance'] = True
    invalid = [np.zeros(N - 1), np.zeros((N, 1)),
               np.full(N, np.nan), np.ones(N, dtype=complex)]
    for bad in invalid:
        try:
            detect(bad)
        except ValueError:
            continue
        raise AssertionError('Invalid input was accepted')
    checks['invalid_input_contract'] = True
    assert N / FS == .1
    checks['frame_duration_is_100ms'] = True
    report = {'versions': {'python': platform.python_version(), 'numpy': np.__version__},
              'fs_hz': FS, 'samples': N, 'frame_ms': N / FS * 1000,
              'window': 'numpy.hanning symmetric Hann',
              'clean_key5': detect(x), 'noisy_key5': detect(tone('5', .02)),
              'silence_key': detect(np.zeros(N))['key'],
              'single_tone_key': detect(single)['key'],
              'quiet_key': detect(.1 * x)['key'],
              'direct_dtft_max_abs_power_error': float(np.max(np.abs(gp - np.abs(direct) ** 2))),
              'fixture_sha256': hashlib.sha256(x.astype('<f8').tobytes()).hexdigest(),
              'checks': checks,
              'limitations': ['No streaming timing, frequency-tolerance certification or talk-off evaluation',
                              'No RTP parsing, codec simulation, real calls or product performance measurements',
                              'Thresholds and amplitudes use arbitrary normalized sample units, not dBm0']}
    print(json.dumps(report, indent=2))


if __name__ == '__main__':
    main()

۴. نتیجهٔ اجرا و پاسخ تمرین‌های مهندسی

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

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

نه بررسی اجرا شدند: تطبیق با DTFT مستقیم، دوازده کلید تمیز، دوازده نمونهٔ نویزی ثابت، رد سکوت، رد تک‌تن، رد نمونهٔ ضعیف، ناوردایی نسبت به وارونگی قطبیت، رد ورودی نامعتبر و کنترل مدت قاب. مقدار خالی در خروجی یعنی این تصمیم‌گیر کلیدی را نپذیرفته است؛ علت قطعی خرابی خط یا گوشی را نشان نمی‌دهد. گزارش کامل زیر خروجی واقعی برنامه است و تصاویر مقاله صرفاً مفهوم را توضیح می‌دهند.

{
  "versions": {
    "python": "3.14.5",
    "numpy": "2.5.4"
  },
  "fs_hz": 8000,
  "samples": 800,
  "frame_ms": 100.0,
  "window": "numpy.hanning symmetric Hann",
  "clean_key5": {
    "key": "5",
    "rms": 0.2996279471455144,
    "low_hz": 770.0,
    "high_hz": 1336.0,
    "amplitudes": [
      0.0001987696223674774,
      0.30000026676473346,
      9.868406473493598e-05,
      4.092732128207952e-06,
      3.971259487903663e-05,
      0.3000002804017213,
      8.66297823924207e-06
    ],
    "margin_db": [
      63.57543256551097,
      77.56386790429639
    ],
    "balance_db": 3.948308769294724e-07
  },
  "noisy_key5": {
    "key": "5",
    "rms": 0.2995664603370092,
    "low_hz": 770.0,
    "high_hz": 1336.0,
    "amplitudes": [
      0.0009941912097064466,
      0.300250156326157,
      0.0016610694946734787,
      0.0011294874229017115,
      0.0007075633810678008,
      0.2995914413075395,
      0.0014221694246296422
    ],
    "margin_db": [
      45.14190879297853,
      46.47156129740117
    ],
    "balance_db": -0.01907679719825723
  },
  "silence_key": null,
  "single_tone_key": null,
  "quiet_key": null,
  "direct_dtft_max_abs_power_error": 6.821210263296962e-12,
  "fixture_sha256": "dddbf10b4cb9d707b2dff2e28e9be24e0a5aa975d86ef4fff283ee72e7a35443",
  "checks": {
    "recurrence_matches_direct_dtft": true,
    "all_12_clean_authored_keys": true,
    "all_12_fixed_noise_fixtures": true,
    "silence_rejected": true,
    "single_tone_rejected": true,
    "quiet_fixture_rejected_by_authored_threshold": true,
    "polarity_invariance": true,
    "invalid_input_contract": true,
    "frame_duration_is_100ms": true
  },
  "limitations": [
    "No streaming timing, frequency-tolerance certification or talk-off evaluation",
    "No RTP parsing, codec simulation, real calls or product performance measurements",
    "Thresholds and amplitudes use arbitrary normalized sample units, not dBm0"
  ]
}

تمرین اول: چرا انتخاب صرفِ بیشینه کافی نیست؟ پاسخ: حتی در سکوت نیز تابع argmax یک خانه را برمی‌گرداند. اگر حداقل دامنه و انرژی را نداشته باشیم، صفرها ممکن است به یک کلید نسبت داده شوند. در این کد سکوت از شرط RMS و دامنه عبور نمی‌کند و نتیجه خالی می‌ماند.

تمرین دوم: با ده برابر کاهش دامنهٔ کلید ۵ چه می‌شود؟ پاسخ: هر دو تن از حدود ۰٫۳ به حدود ۰٫۰۳ می‌رسند؛ نسبت آن‌ها تقریباً ثابت است، اما شرط دامنهٔ حداقل ۰٫۱ نقض می‌شود و خروجی خالی است. این نتیجه نشان می‌دهد آستانهٔ مطلق به بهرهٔ مسیر وابسته است و کالیبراسیون یا راهبرد کنترل بهره باید جدا طراحی شود.

تمرین سوم: آیا ۸۰۰ نمونه همیشه ۱۰۰ میلی‌ثانیه است؟ پاسخ: فقط در ۸۰۰۰ هرتز. در ۱۶۰۰۰ هرتز، همین تعداد نمونه ۵۰ میلی‌ثانیه است و فرکانس‌های محاسبه‌شده با فرض اشتباه، معنای فیزیکی درست ندارند. نرخ نمونه‌برداری باید همراه داده ثبت شود.

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

۵. جایگاه تشخیص صوت در IVR و تلفن هوشمند

تفاوت مسیر صوت و رویداد نام‌گذاری‌شدهٔ telephone-event در سامانهٔ IVR
تصویر مفهومی: تفاوت مسیر صوت و رویداد نام‌گذاری‌شدهٔ telephone-event در سامانهٔ IVR؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

در اتصال واقعی VoIP ابتدا باید نوع ورودی را مشخص کرد. RFC 4733، منتشرشده در دسامبر ۲۰۰۶، قالب RTP برای رویدادهای نام‌گذاری‌شدهٔ تلفنی را تعریف می‌کند. وقتی دریافت‌کننده telephone-event را دریافت و تفسیر می‌کند، همان داده را نباید مانند نمونه‌های صوتی به Goertzel داد. تحلیل صوت و دریافت رویداد دو مسیر متفاوت‌اند؛ تشخیص دوبارهٔ یک کلید در هر دو مسیر می‌تواند منطق منو را دوبار جلو ببرد. این کد هیچ بستهٔ RTP را تجزیه نمی‌کند و پیاده‌سازی RFC محسوب نمی‌شود.

برای ادامهٔ طراحی، مقالهٔ مسیریابی تماس با IVR و DTMF جایگاه رویداد کلید در منو را توضیح می‌دهد. اگر در همان سامانه گفتار هم پذیرفته می‌شود، ارزیابی بازشناسی گفتار فارسی یک مسئلهٔ جدا با داده و معیارهای متفاوت است. موفقیت در تشخیص تن، نشانهٔ دقت تبدیل گفتار به متن یا کیفیت پاسخ‌گویی مبتنی بر پایگاه دانش نیست.

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

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

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