فهرست مقاله
در آزمایش یک مرحلهٔ پردازش صوت، ممکن است موج مرجع و خروجیِ ساختهشده را هر دو داشته باشیم. در این وضعیت میتوان اختلافِ نمونهبهنمونه را اندازه گرفت؛ اما نام SNR بهتنهایی نمیگوید مرجع چیست، قابها چگونه همتراز شدهاند و با سکوت چه میکنیم. این درس با چهار نمونهٔ کاملاً ساختگی، قرارداد محدودی میسازد. هیچ مکالمه، فایل صوت، حذفکنندهٔ نویز یا مدل گفتاری اجرا نمیشود؛ عددِ خروجی شاهد کیفیت محصول نیست.
۱. مرجع و خروجی را روی قاب یکسان قرار دهیم

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

مجموعِ مربع نمونههای مرجع را S و مجموعِ مربع اختلافها را E مینامیم. برای S و E مثبت، قرارداد این درس برابر 10log10(S/E) است. تقسیم هر دو مجموع بر تعدادِ یکسان نمونهها، نسبت را تغییر نمیدهد. اگر از تعبیرِ توان متوسط استفاده شود، دامنه، واحد، نرخ و پنجرهٔ مقایسه باید مشترک باشند؛ این دو مجموع را بدون مقیاس زمانی، انرژی فیزیکیِ اندازهگیریشده برحسب ژول معرفی نمیکنیم.
در مستندات رسمی snr، ورودیِ دوآرایهایِ سیگنال و برآورد نویز، نسبت مجموع مربع اندازهها را به کار میبرد. شکلِ تکآرایهایِ همان نام برای سیگنال سینوسی، تحلیل طیفی و سیاست متفاوتِ هارمونیکها دارد. مثال ما از مرجعِ معلوم و اختلافِ خروجی استفاده میکند؛ فراخوانی MATLAB، برآورد طیفی نویز یا انطباق همهٔ حالتهای آن تابع ادعا نشده است.
پاسخ دستی روشن است: مربعهای مرجع، مجموعِ ۱۲۰ تقسیم بر ۲۵۶، یعنی پانزدهسیودوم میدهند. چهار اختلافِ یکشانزدهمی، مجموع مربعِ یکشصتوچهارم دارند. نسبتِ S به E دقیقاً سی است؛ حاصلِ لگاریتمی در اجرای ثبتشده حدود ۱۴٫۷۷۱۲۱۳ دسیبل شد. ضریب ده برای نسبتِ این مربعهاست؛ گذاشتنِ ضریب بیست روی همان نسبت، مقدار را اشتباه دو برابر میکند.
۳. سکوت، تطابق کامل و تفاوت بهره را صریح گزارش کنیم

اگر مرجعِ غیرصفر دقیقاً بازسازی شود، E صفر است و نسبتِ متناهی نداریم؛ حدِ مقدار به مثبت بینهایت میرود. خروجیِ ما وضعیت exact_match و مقدار تهی برای عدد دسیبل میدهد، نه صفر یا سقفِ قراردادیِ پنهان. اگر S صفر باشد، وضعیت silent_reference داریم؛ حتی وقتی خروجی غیرصفر است، برای مرجعِ ساکت امتیاز عددی گزارش نمیکنیم. این سیاستِ محدودِ ماست؛ نسبتِ صفر به صفر تعریفشده نمیشود.
در کد رسمی SNR در TorchMetrics، خطا از اختلافِ هدف و پیشبینی ساخته میشود، امکان حذف میانگین وجود دارد و epsilonِ نوع داده به صورت و مخرج افزوده میشود. ما میانگین را حذف نمیکنیم و epsilon نداریم؛ بنابراین رفتارِ صفر و عددِ مرزیِ آن کتابخانه را بازتولیدشده نمیدانیم. خودِ کتابخانه نیز در این تمرین اجرا نشده است.
اگر خروجی فقط نصفِ مرجع باشد، اختلاف برابر منفی نصف مرجع و E یکچهارم S میشود؛ نسبت چهار و جواب حدود ۶٫۰۲۰۶ دسیبل است. پس این معیار، اختلافِ بهره را خطا میگیرد؛ معیارِ مستقل از مقیاس نیست. در مقابل، ضرب هر دو قاب در همان ضریبِ غیرصفر، تا وقتی داخل دامنهٔ قرارداد بمانند، نسبت را حفظ میکند. ثابت بودن یا نبودن میانگین نیز باید آشکار باشد؛ روی دو قاب ثابت، حذف میانگین مسئلهٔ دیگری میسازد.
۴. کد را با شمارش صحیحِ مستقل بسنجیم

فایل reference_audio_snr.py با Python 3.14.5 و کتابخانهٔ استاندارد اجرا شده است. هر AudioBlock هنگام مقایسه اعتبارسنجی میشود؛ صرفِ داشتنِ dataclass کافی نیست. نمونهها باید tuple با یک تا شصتوچهار عضو، دقیقاً int یا float و داخل منفی یک تا مثبت یک باشند؛ bool، مقدار نامتناهی و تبدیلِ خودکار رشته پذیرفته نیست. نرخ فقط هشت، شانزده یا چهلوهشت کیلوهرتز و مبدأ، صحیحِ صفر تا یکمیلیون است.
"""Reference-based finite sample comparison; no recorded or perceptual audio test."""
from dataclasses import dataclass
from decimal import Decimal, localcontext
from fractions import Fraction
from itertools import product
import json
import math
import platform
@dataclass(frozen=True)
class AudioBlock:
samples: tuple
sample_rate_hz: int = 8000
start_frame: int = 0
def validate_block(block):
if type(block) is not AudioBlock:
raise TypeError("expected an AudioBlock")
if (type(block.sample_rate_hz) is not int or
block.sample_rate_hz not in (8000, 16000, 48000)):
raise ValueError("sample rate must be an allowed exact integer")
if type(block.start_frame) is not int or not 0 <= block.start_frame <= 1_000_000:
raise ValueError("start frame must be an exact integer in range")
if type(block.samples) is not tuple or not 1 <= len(block.samples) <= 64:
raise ValueError("samples must be a tuple of length 1..64")
values = []
for x in block.samples:
if type(x) not in (int, float):
raise TypeError("sample must be int or float, not bool")
if not -1 <= x <= 1: # Reject nonfinite/huge values before float conversion.
raise ValueError("sample outside [-1, 1]")
value = float(x)
if not (value * 1024).is_integer():
raise ValueError("sample must be on the authored 1/1024 amplitude grid")
values.append(value)
return tuple(values)
def compare_snr(reference, estimate):
target = validate_block(reference)
predicted = validate_block(estimate)
if (reference.sample_rate_hz != estimate.sample_rate_hz or
reference.start_frame != estimate.start_frame or
len(target) != len(predicted)):
raise ValueError("sample rate, start frame and frame count must agree")
signal_sum = math.fsum(x * x for x in target)
error_sum = math.fsum((y - x) ** 2 for x, y in zip(target, predicted))
# These three states are our explicit policy, not all libraries' API behavior.
if signal_sum == 0:
status, db = "silent_reference", None
elif error_sum == 0:
status, db = "exact_match", None
else:
status, db = "finite", 10 * math.log10(signal_sum / error_sum)
return {"status": status, "snr_db": db,
"signal_square_sum": signal_sum, "error_square_sum": error_sum,
"sample_rate_hz": reference.sample_rate_hz,
"start_frame": reference.start_frame, "frame_count": len(target)}
def integer_reference(target_codes, predicted_codes, denominator):
"""Independent literal integer codes; no validation/fsum/compare_snr calls."""
signal_units = sum(x * x for x in target_codes)
error_units = sum((y - x) * (y - x)
for x, y in zip(target_codes, predicted_codes))
if signal_units == 0:
status, db = "silent_reference", None
elif error_units == 0:
status, db = "exact_match", None
else:
with localcontext() as context:
context.prec = 70
db = float(Decimal(10) * (Decimal(signal_units).log10() -
Decimal(error_units).log10()))
status = "finite"
return (status, db, Fraction(signal_units, denominator ** 2),
Fraction(error_units, denominator ** 2))
def rejects(exception, function, *args):
try:
function(*args)
except exception:
return True
return False
def close(a, b):
return math.isclose(a, b, rel_tol=0, abs_tol=1e-12)
def run_example():
target_codes = (6, -2, 4, -8)
predicted_codes = (7, -3, 5, -9)
target = AudioBlock(tuple(x / 16 for x in target_codes))
predicted = AudioBlock(tuple(x / 16 for x in predicted_codes))
hand = compare_snr(target, predicted)
checks = {}
checks["hand_exact_sums_ratio_thirty_and_log"] = (
hand["signal_square_sum"] == 15 / 32 and
hand["error_square_sum"] == 1 / 64 and
hand["signal_square_sum"] / hand["error_square_sum"] == 30 and
close(hand["snr_db"], 14.771212547196624))
code_vectors = list(product((-2, -1, 0, 1, 2), repeat=2))
oracle_passed, largest_db_difference = True, 0.0
for a in code_vectors:
for b in code_vectors:
actual = compare_snr(AudioBlock(tuple(x / 4 for x in a)),
AudioBlock(tuple(x / 4 for x in b)))
status, db, signal, error = integer_reference(a, b, 4)
same = (actual["status"] == status and
Fraction.from_float(actual["signal_square_sum"]) == signal and
Fraction.from_float(actual["error_square_sum"]) == error)
if db is None:
same = same and actual["snr_db"] is None
else:
largest_db_difference = max(largest_db_difference,
abs(actual["snr_db"] - db))
same = same and close(actual["snr_db"], db)
oracle_passed = oracle_passed and same
checks["integer_decimal_oracle_625_pairs_one_group"] = oracle_passed
checks["metadata_mismatch_not_silently_aligned_or_resampled"] = all(
rejects(ValueError, compare_snr, target, other)
for other in (AudioBlock(predicted.samples, 16000),
AudioBlock(predicted.samples, 8000, 1),
AudioBlock(predicted.samples[:-1])))
checks["metadata_types_and_bounds_are_explicit"] = all(
rejects(ValueError, compare_snr, target, AudioBlock(predicted.samples, rate, start))
for rate, start in ((True, 0), (8000.0, 0), (44100, 0),
(8000, False), (8000, -1), (8000, 1_000_001)))
bad_samples = ((), (0,) * 65, [0, 1], (True,), ("0",), (Fraction(1, 4),),
(math.nan,), (math.inf,), (-math.inf,), (10 ** 400,),
(-10 ** 400,), (1.1,), (0.1,), (math.nextafter(1 / 1024, 0),))
checks["sample_container_type_range_and_grid_rejected"] = all(
rejects((TypeError, ValueError), compare_snr, AudioBlock(samples), AudioBlock(samples))
for samples in bad_samples)
smallest = compare_snr(AudioBlock((1 / 1024,)), AudioBlock((0,)))
largest = compare_snr(AudioBlock((1,) * 64, 48000, 1_000_000),
AudioBlock((-1,) * 64, 48000, 1_000_000))
checks["finite_closed_grid_and_metadata_bounds"] = (
smallest["status"] == largest["status"] == "finite" and
smallest["snr_db"] == 0 and math.isfinite(largest["snr_db"]))
exact = compare_snr(target, target)
checks["exact_match_is_explicit_without_epsilon_or_fake_zero_db"] = (
exact["status"] == "exact_match" and exact["snr_db"] is None and
exact["error_square_sum"] == 0 and exact["signal_square_sum"] > 0)
silence = compare_snr(AudioBlock((0, 0)), AudioBlock((0, 0)))
silent_error = compare_snr(AudioBlock((0, 0)), AudioBlock((1 / 4, 0)))
checks["silent_reference_has_no_defined_score_even_if_estimate_nonzero"] = (
silence["status"] == silent_error["status"] == "silent_reference" and
silence["snr_db"] is silent_error["snr_db"] is None and
silence["error_square_sum"] == 0 < silent_error["error_square_sum"])
gain = compare_snr(target, AudioBlock(tuple(x / 2 for x in target.samples)))
checks["gain_only_change_counts_as_residual_error"] = (
gain["signal_square_sum"] / gain["error_square_sum"] == 4 and
close(gain["snr_db"], 6.020599913279624))
scaled = compare_snr(AudioBlock(tuple(x / 2 for x in target.samples)),
AudioBlock(tuple(x / 2 for x in predicted.samples)))
checks["common_amplitude_scaling_preserves_ratio"] = close(
scaled["snr_db"], hand["snr_db"])
order = (3, 1, 0, 2)
joint = compare_snr(AudioBlock(tuple(target.samples[i] for i in order)),
AudioBlock(tuple(predicted.samples[i] for i in order)))
shifted = compare_snr(target, AudioBlock(target.samples[1:] + target.samples[:1]))
checks["joint_order_preserved_but_unmatched_shift_changes_comparison"] = (
joint["signal_square_sum"] == hand["signal_square_sum"] and
joint["error_square_sum"] == hand["error_square_sum"] and
close(joint["snr_db"], hand["snr_db"]) and
shifted["status"] == "finite" and shifted["snr_db"] < 0)
dc = compare_snr(AudioBlock((1 / 4, 1 / 4)), AudioBlock((1 / 2, 1 / 2)))
checks["no_implicit_demeaning_of_constant_blocks"] = (
dc["status"] == "finite" and dc["snr_db"] == 0 and
dc["signal_square_sum"] == dc["error_square_sum"] == 1 / 8)
zero_estimate = compare_snr(target, AudioBlock((0,) * len(target.samples)))
checks["zero_db_is_equal_sums_not_an_error_free_signal"] = (
zero_estimate["snr_db"] == 0 and
zero_estimate["error_square_sum"] == zero_estimate["signal_square_sum"] > 0)
before = (target.samples, predicted.samples, target.sample_rate_hz, target.start_frame)
compare_snr(target, predicted)
checks["input_data_and_metadata_not_modified"] = before == (
target.samples, predicted.samples, target.sample_rate_hz, target.start_frame)
checks["all_three_states_serialize_as_strict_json"] = all(
json.loads(json.dumps(x, allow_nan=False)) == x
for x in (hand, exact, silence, silent_error))
assert all(type(v) is bool and v for v in checks.values()), checks
return {"python_version": platform.python_version(),
"dependencies": "standard library only; no MATLAB or TorchMetrics execution",
"amplitude_unit": "dimensionless relative amplitude, authored 1/1024 grid",
"data_origin": "original synthetic reference and reconstructed blocks",
"hand_reference": target.samples, "hand_estimate": predicted.samples,
"hand_result": hand, "half_gain_result": gain,
"unmatched_one_frame_cyclic_shift_result": shifted,
"silent_reference_result": silence, "exact_match_result": exact,
"oracle_pairs_inside_one_group": len(code_vectors) ** 2,
"oracle_decimal_precision": 70,
"largest_oracle_db_difference": largest_db_difference,
"oracle_db_absolute_tolerance": 1e-12,
"checks": checks, "unique_check_groups": len(checks)}
if __name__ == "__main__":
print(json.dumps(run_example(), ensure_ascii=False, indent=2, allow_nan=False))
برای کوچک و روشن ماندنِ آزمایش عددی، هر دامنه مضربِ دقیقِ یکهزاروبیستوچهارم است. عدد ۰٫۱ به این شبکه تعلق ندارد و رد میشود؛ هیچ کوانتیزهکردن یا گردکردنِ پنهانی نداریم. این قرارداد، بیتعمق یک کدک یا همهٔ فایلهای PCM نیست. کوچکترین مقدار غیرصفر و طول محدود، دامنهای مشخص برای مربعها میسازند؛ پذیرشِ شناورهای دلخواه با دامنهٔ بسیار گسترده به بررسی دیگری نیاز دارد.
مسیر اصلی از جمعِ شناورِ fsum و log10 استفاده میکند. راهنمای math پایتون نگهداریِ جمعهای جزئی و محدودیتهای عددی این توابع را توضیح میدهد. مرجع مستقل، کدهای صحیحِ نمونهها را مستقیم مربع میکند؛ جمعها را با Fraction نگه میدارد و لگاریتم را با Decimal و دقت هفتاد رقم میسنجد. این مسیر، تابع مقایسه، اعتبارسنج یا fsum مسیر اصلی را برای ساخت انتظار صدا نمیزند.
از پنج مقدارِ کوچک، همهٔ بردارهای دوعضوی و تمام جفتهای آنها ساخته میشوند: ۶۲۵ مقایسه، فقط یک گروه از پانزده گروه اجراییاند. بزرگترین اختلافِ دسیبل حدود ۸٫۸۸ ضربدر ده به توان منفی شانزده بود؛ تلورانسِ مطلق یکتریلیونیم دسیبل، فقط حدِ همین مقایسهٔ محاسباتی است. پاسخ دستی، خطای فراداده، سکوت، بهره، مرزهای نوع و شبکه و دستنخوردن ورودی نیز بررسی شدهاند.
{
"python_version": "3.14.5",
"dependencies": "standard library only; no MATLAB or TorchMetrics execution",
"amplitude_unit": "dimensionless relative amplitude, authored 1/1024 grid",
"data_origin": "original synthetic reference and reconstructed blocks",
"hand_reference": [
0.375,
-0.125,
0.25,
-0.5
],
"hand_estimate": [
0.4375,
-0.1875,
0.3125,
-0.5625
],
"hand_result": {
"status": "finite",
"snr_db": 14.771212547196624,
"signal_square_sum": 0.46875,
"error_square_sum": 0.015625,
"sample_rate_hz": 8000,
"start_frame": 0,
"frame_count": 4
},
"half_gain_result": {
"status": "finite",
"snr_db": 6.020599913279624,
"signal_square_sum": 0.46875,
"error_square_sum": 0.1171875,
"sample_rate_hz": 8000,
"start_frame": 0,
"frame_count": 4
},
"unmatched_one_frame_cyclic_shift_result": {
"status": "finite",
"snr_db": -5.642714304385627,
"signal_square_sum": 0.46875,
"error_square_sum": 1.71875,
"sample_rate_hz": 8000,
"start_frame": 0,
"frame_count": 4
},
"silent_reference_result": {
"status": "silent_reference",
"snr_db": null,
"signal_square_sum": 0.0,
"error_square_sum": 0.0,
"sample_rate_hz": 8000,
"start_frame": 0,
"frame_count": 2
},
"exact_match_result": {
"status": "exact_match",
"snr_db": null,
"signal_square_sum": 0.46875,
"error_square_sum": 0.0,
"sample_rate_hz": 8000,
"start_frame": 0,
"frame_count": 4
},
"oracle_pairs_inside_one_group": 625,
"oracle_decimal_precision": 70,
"largest_oracle_db_difference": 8.881784197001252e-16,
"oracle_db_absolute_tolerance": 1e-12,
"checks": {
"hand_exact_sums_ratio_thirty_and_log": true,
"integer_decimal_oracle_625_pairs_one_group": true,
"metadata_mismatch_not_silently_aligned_or_resampled": true,
"metadata_types_and_bounds_are_explicit": true,
"sample_container_type_range_and_grid_rejected": true,
"finite_closed_grid_and_metadata_bounds": true,
"exact_match_is_explicit_without_epsilon_or_fake_zero_db": true,
"silent_reference_has_no_defined_score_even_if_estimate_nonzero": true,
"gain_only_change_counts_as_residual_error": true,
"common_amplitude_scaling_preserves_ratio": true,
"joint_order_preserved_but_unmatched_shift_changes_comparison": true,
"no_implicit_demeaning_of_constant_blocks": true,
"zero_db_is_equal_sums_not_an_error_free_signal": true,
"input_data_and_metadata_not_modified": true,
"all_three_states_serialize_as_strict_json": true
},
"unique_check_groups": 15
}
۵. عدد SNR را کیفیت ادراکی مکالمه ننامیم

در چرخشِ یکفریمیِ خودِ مرجع، با حفظِ شناسنامهٔ ظاهراً برابر، S همان پانزدهسیودوم میماند ولی E به پنجاهوپنجسیودوم میرسد. مقدار حدود منفی ۵٫۶۴۲۷۱۴ دسیبل است. این شاهد، حساسیت به جفتشدن نمونهها را نشان میدهد؛ همترازیِ خودکار برای بهتر نشاندادن امتیاز انجام نشده است. همچنین خروجیِ تماماً صفر برای مرجع مثبت، S و E برابر و صفر دسیبل دارد؛ این به معنی نبودِ خطا نیست.
در دادهٔ واقعی، اختلاف ممکن است نویز افزوده، تغییر بهره، تأخیر، اعوجاج یا خطایِ خودِ مرجع را با هم داشته باشد. بدون دسترسی به مرجع مناسب، همین عدد را از یک مکالمهٔ ناشناخته استخراج نمیکنیم. معیار شنیداری، فهمپذیری و خطای تشخیص گفتار آزمایشهای جدا میخواهند. آزمایش FFT و پنجرهٔ Hann نیز نسبتِ نشت طیفی را صریحاً SNR نمینامید؛ اشتراکِ واحد دسیبل، دو تعریف را یکی نمیکند.
پرسش اول: چرا ضریب ده داریم؟ پاسخ: S/E نسبتِ مجموع مربعهاست؛ نسبتِ دامنهٔ مؤثرِ متناظر رادیکال سی است و ضریب بیست روی همان رادیکال، جواب برابر میدهد.
پرسش دوم: تطابق کامل یعنی صفر دسیبل؟ پاسخ: خیر؛ برای مرجع مثبت، E صفر و حد نسبت نامتناهی است. وضعیت exact_match عددِ تهی دارد؛ صفر دسیبل برای S و E مثبت و برابر است.
پرسش سوم: برابر بودن طول، همترازی را ثابت میکند؟ پاسخ: خیر؛ نرخ و مبدأ هم لازماند و حتی برچسب درستنما جای شاهدِ محتوایی را نمیگیرد. چرخشِ عمدی، نمونهٔ همین تفاوت است.
پرسش چهارم: SNR بزرگتر، گفتار واضحتر یا ASR دقیقتر را تضمین میکند؟ پاسخ: خیر؛ این محاسبه فقط قراردادِ موجِ مرجع و خطا را میسنجد. منابع در ۲۵ مهر ۱۴۰۵ خوانده شدند؛ نسخهٔ MATLAB مرجع R2026b و مستندات Python برابر 3.14.8 از اجرای واقعی جدا هستند. تاریخ نخستین انتشار صفحات معلوم نیست و این آموزش، خبر تازه یا آزمون محصول نیست.
منابع و مطالعهٔ بیشتر
- MathWorks؛ نسبت مجموع مربع سیگنال و نویز در ورودی دوآرایهای
- Lightning؛ کد رسمی SNR، خطای مرجع، میانگین و epsilon
- Python math؛ جمع شناور و لگاریتم دهدهی
بررسی منابع: ۲۵ مهر ۱۴۰۵. تحلیلها و پیشنهادهای اجرایی، جمعبندی نویسندهاند.

