۱. قاب صوت را با مدت، نرخ نمونه‌برداری و واحد تعریف کنید

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

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

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

اگر ورودی شما ابتدا باید از نرخ دیگری به ۸ کیلوهرتز تبدیل شود، مسئلهٔ فیلتر و aliasing را جدا حل کنید؛ مقالهٔ تبدیل نرخ نمونه‌برداری صوت با آزمون مستقل این مرحله را توضیح می‌دهد. افزودن صفر به FFT جای تبدیل نرخ یا دریافت دادهٔ تازه را نمی‌گیرد.

۲. پنجرهٔ Hann چه چیزی را عوض می‌کند و دامنه چگونه اصلاح می‌شود؟

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

راهنمای تحلیل طیفی SciPy محدود بودن ثبت را یکی از زمینه‌های نشت طیفی توضیح می‌دهد. در مثال ما هر نمونه پیش از تبدیل در یک وزن ضرب می‌شود. پنجرهٔ مستطیلی همهٔ وزن‌ها را یک می‌گیرد؛ Hann وزن لبه‌ها را کم می‌کند. هدف این انتخاب، تغییر رفتار طیف قاب است، نه پاک کردن واقعی نویز ورودی. در مقابل کاهش برخی لوب‌های جانبی، لوب اصلی می‌تواند پهن‌تر شود؛ بنابراین وعدهٔ «بهترین پنجره برای همهٔ مسائل» درست نیست.

طبق مستندات Hann، گزینهٔ تناوبی برای تحلیل طیفی در دسترس است. اینجا همان نوع را با فرمول صریح می‌سازیم: وزن نمونهٔ n برابر ۰٫۵ منهای ۰٫۵ ضربدر کسینوسِ ۲πn/N است. مخرج N با مخرج N−1 در نوع متقارن فرق دارد. میانگین این پنجره برای طول انتخاب‌شده ۰٫۵ می‌شود؛ این نتیجه در کد بررسی می‌شود و استفادهٔ تصادفی از نوع دیگر پنهان نمی‌ماند.

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

۳. کد کامل پایتون و آزمون‌های مستقل قرارداد محاسبه

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

کد زیر با Python 3.14.5 و NumPy 2.4.6 اجرا شده است و به SciPy نیاز ندارد. مستندات جاری هنگام مطالعه نسخهٔ NumPy 2.5 و SciPy 1.18.0 را نشان می‌دادند؛ این نسخه‌های مرجع را با محیط اجرای واقعی اشتباه نمی‌گیریم. فایل را با نام fft_window.py ذخیره و با پایتون اجرا کنید. خروجی JSON شامل پارامترهای آزمایش، مقادیر قله و نه کنترل مشخص است. برای انتقال به برنامهٔ کاربردی، ابتدا همین قراردادها را در محیط خود تکرار کنید.

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

"""Synthetic telephone-tone spectrum checks; no recorded calls or product benchmark."""
import json
import math
import platform
import numpy as np


def amplitude_spectrum(samples, fs, window, nfft=None):
    x = np.asarray(samples, dtype=float)
    w = np.asarray(window, dtype=float)
    if (x.ndim != 1 or x.size < 4 or x.shape != w.shape
            or not np.all(np.isfinite(x)) or not np.all(np.isfinite(w))
            or not math.isfinite(fs) or fs <= 0 or np.sum(w) <= 0):
        raise ValueError("Require finite 1-D samples, positive fs and window sum")
    nfft = len(x) if nfft is None else nfft
    if isinstance(nfft, bool) or not isinstance(nfft, int) or nfft < len(x):
        raise ValueError("nfft must be an integer at least the record length")
    transformed = np.fft.rfft(x * w, n=nfft)
    amplitude = np.abs(transformed) / np.sum(w)
    if nfft % 2 == 0:
        amplitude[1:-1] *= 2
    else:
        amplitude[1:] *= 2
    return np.fft.rfftfreq(nfft, d=1 / fs), amplitude


def run_example():
    fs, count, amplitude = 8000.0, 800, 0.4
    n = np.arange(count)
    rectangular = np.ones(count)
    # Periodic Hann for spectral analysis; no SciPy dependency.
    hann = 0.5 - 0.5 * np.cos(2 * np.pi * n / count)
    on_bin = amplitude * np.cos(2 * np.pi * 1000 * n / fs)
    off_bin = amplitude * np.cos(2 * np.pi * 1003 * n / fs)
    frequency, rect = amplitude_spectrum(off_bin, fs, rectangular)
    _, tapered = amplitude_spectrum(off_bin, fs, hann)
    padded_f, padded_a = amplitude_spectrum(off_bin, fs, hann, 8000)
    _, on_bin_a = amplitude_spectrum(on_bin, fs, hann)
    _, dc = amplitude_spectrum(np.full(count, 0.2), fs, rectangular)
    _, nyquist = amplitude_spectrum(0.3 * (-1.0) ** n, fs, rectangular)
    _, odd = amplitude_spectrum(off_bin, fs, hann, 801)
    guard = np.abs(frequency - 1003.0) >= 40.0
    # This is a declared far-bin amplitude ratio, NOT THD, SNR or MOS.
    rect_ratio = float(np.max(rect[guard]) / amplitude)
    hann_ratio = float(np.max(tapered[guard]) / amplitude)
    rejected = 0
    for bad_x, bad_fs, bad_w, bad_n in [
        ([1.0, float('nan'), 2, 3], fs, np.ones(4), None),
        (on_bin, 0.0, hann, None),
        (on_bin, fs, hann[:-1], None),
        (on_bin, fs, np.zeros(count), None),
        (on_bin, fs, hann, 799),
    ]:
        try:
            amplitude_spectrum(bad_x, bad_fs, bad_w, bad_n)
        except ValueError:
            rejected += 1
    checks = {
        'periodic_hann_coherent_gain_half': bool(np.isclose(hann.mean(), 0.5)),
        'coherent_tone_peak_amplitude': bool(np.isclose(on_bin_a[100], 0.4)),
        'dc_not_doubled': bool(np.isclose(dc[0], 0.2)),
        'nyquist_not_doubled': bool(np.isclose(nyquist[-1], 0.3)),
        'padding_preserves_old_frequency_samples': bool(np.allclose(padded_a[::10], tapered)),
        'padding_changes_grid_not_record_duration': bool(np.isclose(padded_f[1], 1.0) and count / fs == 0.1),
        'hann_reduces_declared_far_bin_ratio': hann_ratio < rect_ratio / 10,
        'odd_transform_has_expected_length': len(odd) == 401,
        'invalid_inputs_rejected': rejected == 5,
    }
    assert all(checks.values()), checks
    return {
        'runtime': {'python': platform.python_version(), 'numpy': np.__version__, 'scipy_used': False},
        'synthetic_only': True, 'sample_rate_hz': fs, 'record_samples': count,
        'record_duration_seconds': count / fs, 'unpadded_grid_hz': fs / count,
        'tone_frequency_hz': 1003.0, 'tone_peak_full_scale': amplitude,
        'rectangular_peak_full_scale': float(rect.max()),
        'hann_peak_full_scale': float(tapered.max()),
        'padded_hann_peak_full_scale': float(padded_a.max()),
        'padded_peak_frequency_hz': float(padded_f[np.argmax(padded_a)]),
        'far_bin_guard_hz': 40.0,
        'rectangular_far_amplitude_ratio': rect_ratio,
        'hann_far_amplitude_ratio': hann_ratio,
        'checks': checks,
    }


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

۴. خروجی واقعی و تفاوت شبکهٔ متراکم با تفکیک بیشتر

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

در اجرای ثبت‌شده، بیشینهٔ دامنه برای تن ۱۰۰۳ هرتز با پنجرهٔ مستطیلی حدود ۰٫۳۴۳۶۱۹ و با Hann حدود ۰٫۳۷۷۳۱۶ شد؛ دامنهٔ ساخته‌شده ۰٫۴ بود. پس اصلاح بهرهٔ پنجره به‌تنهایی خطای قرار گرفتن بین نقاط شبکه را حذف نکرد. با افزایش طول تبدیل به ۸۰۰۰، شبکه یک‌هرتزی شد و قله در این مثال خاص روی ۱۰۰۳ هرتز با دامنهٔ تقریبی ۰٫۴ قرار گرفت. چون عمداً فرکانس را عدد صحیح انتخاب کرده‌ایم، این نتیجه مجوز تعمیم به هر فرکانس یا هر قاب نیست.

آزمون مهم‌تر این است که هر دهمین نقطهٔ طیف جدید با نقطهٔ متناظر طیف قبلی برابر می‌ماند. داده همان ۸۰۰ نمونه و مدت ثبت همان ۰٫۱ ثانیه است. مستندات numpy.fft.rfft توضیح می‌دهند که n بزرگ‌تر ورودی را با صفر پر می‌کند. در نتیجه، شبکهٔ نمونه‌برداری از طیف متراکم‌تر می‌شود؛ اطلاعات اندازه‌گیری‌شدهٔ تازه‌ای نداریم و دو تن حل‌نشده الزاماً جدا نمی‌شوند. مخرج اصلاح دامنه همچنان مجموع پنجرهٔ اصلی است، نه طول جدید تبدیل.

برای سنجش محدود نشت، نقاطی را که دست‌کم ۴۰ هرتز با تن فاصله دارند انتخاب می‌کنیم و بیشترین دامنهٔ آن‌ها را بر ۰٫۴ تقسیم می‌کنیم. نسبت برای مستطیلی حدود ۰٫۰۶۰۵۶۷ و برای Hann حدود ۰٫۰۰۳۴۲۴ شد. این معیار تألیفی، صرفاً نسبت بیشینهٔ دامنه در مجموعهٔ مشخصی از نقاط است؛ SNR، THD، امتیاز کیفیت مکالمه یا دقت ASR نیست. تغییر حریم ۴۰ هرتزی تعریف آزمایش را عوض می‌کند و باید همراه نتیجه گزارش شود.

{
  "runtime": {
    "python": "3.14.5",
    "numpy": "2.4.6",
    "scipy_used": false
  },
  "synthetic_only": true,
  "sample_rate_hz": 8000.0,
  "record_samples": 800,
  "record_duration_seconds": 0.1,
  "unpadded_grid_hz": 10.0,
  "tone_frequency_hz": 1003.0,
  "tone_peak_full_scale": 0.4,
  "rectangular_peak_full_scale": 0.3436185360668283,
  "hann_peak_full_scale": 0.37731591211653226,
  "padded_hann_peak_full_scale": 0.40000000448984385,
  "padded_peak_frequency_hz": 1003.0,
  "far_bin_guard_hz": 40.0,
  "rectangular_far_amplitude_ratio": 0.060566517080199875,
  "hann_far_amplitude_ratio": 0.0034241139454532304,
  "checks": {
    "periodic_hann_coherent_gain_half": true,
    "coherent_tone_peak_amplitude": true,
    "dc_not_doubled": true,
    "nyquist_not_doubled": true,
    "padding_preserves_old_frequency_samples": true,
    "padding_changes_grid_not_record_duration": true,
    "hann_reduces_declared_far_bin_ratio": true,
    "odd_transform_has_expected_length": true,
    "invalid_inputs_rejected": true
  }
}

۵. پاسخ تمرین‌ها و حدود استفاده در تلفن هوشمند

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

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

تمرین دوم: اگر تمام مؤلفه‌ها را دو برابر کنیم چه خطایی رخ می‌دهد؟ پاسخ: دامنهٔ DC برابر ۰٫۲ به‌اشتباه ۰٫۴ و دامنهٔ نایکوئیست ۰٫۳ به‌اشتباه ۰٫۶ می‌شود. کنترل‌های جداگانهٔ کد دقیقاً این خطا را آشکار می‌کنند. تمرین سوم: آیا تقسیم بر ۸۰۰۰ به‌جای مجموع پنجره، پس از افزودن صفر درست است؟ پاسخ: خیر؛ در این مثال مجموع Hann برابر ۴۰۰ است. تغییر مخرج به طول جدید مقیاس دامنه را خراب می‌کند، هرچند مکان قله همچنان باورپذیر دیده شود.

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

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

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

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