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

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

مسیر اصلی از ساختار DF-II transposed استفاده میکند. خروجی از ضریبِ نمونهٔ جاری و حالتِ نخست ساخته میشود. حالتِ نخستِ تازه، سهم ضریبِ دومِ ورودی، بازخوردِ خروجیِ تازه و حالتِ دومِ قدیمی را جمع میکند؛ حالتِ دومِ تازه نیز از ضریبِ سومِ ورودی و بازخوردِ دوم ساخته میشود. ابتدا هر دو مقدارِ تازه محاسبه و سپس جایگزین میشوند.
توضیح کامل lfilter در SciPy همین خانوادهٔ معادلههای حالت را مستند میکند. دو حالتِ ما دو ورودیِ قبلی یا دو خروجیِ قبلی نیستند؛ ترکیبهایی از سهمهای ذخیرهشدهاند. پرکردنِ آنها با تاریخچهای که برای یک FIR درست بوده، لزوماً قراردادِ این ساختار را برآورده نمیکند. نامِ عمومیِ «حافظهٔ فیلتر» برای جابهجاییِ این نمایشها کافی نیست.
حالتِ پیشفرض صفر است؛ یعنی سکونِ آغازینِ همین محاسبه را فرض میکنیم. با حالتِ صریحِ یکهشتم و منفی یکشانزدهم و سه ورودیِ صفر، خروجی یکهشتم، صفر و منفی یکشصتوچهارم میشود. پس صفر بودنِ ورودی، با وجود حافظهٔ غیرصفر، خروجی را فوراً صفر نمیکند. این شاهد، اندازهگیریِ انرژی یا مقدارِ فیزیکیِ پیش از ضبط نیست.
هر قاب با تعدادِ ورودیِ خودش خروجی میدهد و حالتِ نهایی را جدا برمیگرداند. قابِ خالی نمونهای تولید نمیکند و حالت را نگه میدارد. بر خلافِ یک FIR محدود، افزودنِ دو صفر بهطور عمومی پایانِ پاسخِ این فیلترِ بازخورددار را تضمین نمیکند؛ تعدادِ دو حالت با طولِ محدودِ پاسخِ ضربه برابر نیست.
۳. کد و مرز تداوم قاب

کد با Python 3.14.5 و فقط کتابخانهٔ استاندارد اجرا شده است. نمونهها باید tuple واقعیِ حداکثر ۳۲ عضوی و حالت، tuple واقعیِ دو عضوی باشد. نمونهها فقط int یا float دقیق و متناهی با قدرِ حداکثر چهار هستند. bool، زیرکلاسِ عدد، رشته و مختلط تبدیلِ ضمنی نمیشوند. حالتِ صریح باید به float متناهی تبدیل شود؛ حالتِ بسیار بزرگ ممکن است در محاسبه خطا بسازد.
"""Original fixed-profile IIR arithmetic example; no audio/model/library execution."""
from fractions import Fraction
import itertools
import json
import math
import platform
import sys
B = (0.25, 0.5, 0.25)
A = (1.0, -0.5, 0.125)
ABS, REL = 1e-12, 1e-12
def number(value, bound=None):
if type(value) not in (int, float):
raise TypeError("actual Python int/float required")
if bound is not None and not -bound <= value <= bound:
raise ValueError("sample outside chosen dimensionless range")
try:
value = float(value)
except OverflowError as exc:
raise ValueError("value cannot be represented as finite float") from exc
if not math.isfinite(value):
raise ValueError("finite value required")
return value
def biquad(samples, state=(0.0, 0.0)):
"""Return exactly N samples and two DF-II-transposed internal states."""
if type(samples) is not tuple or type(state) is not tuple:
raise TypeError("actual tuples required")
if len(samples) > 32 or len(state) != 2:
raise ValueError("at most32 samples and exactly two states required")
xs = tuple(number(x, 4) for x in samples)
z0, z1 = (number(x) for x in state)
output = []
for x in xs:
try:
y = math.fsum((B[0] * x, z0))
next0 = math.fsum((B[1] * x, -A[1] * y, z1))
next1 = math.fsum((B[2] * x, -A[2] * y))
except OverflowError as exc:
raise ArithmeticError("nonfinite arithmetic; no block result returned") from exc
if not all(math.isfinite(v) for v in (y, next0, next1)):
raise ArithmeticError("nonfinite arithmetic; no block result returned")
output.append(y)
z0, z1 = next0, next1
return tuple(output), (z0, z1)
def global_fraction_reference(samples, state=(0, 0)):
"""Solve the entire finite difference-equation system via Gauss-Jordan."""
size = len(samples)
if not size:
return ()
xs = tuple(map(Fraction, samples))
bs = (Fraction(1, 4), Fraction(1, 2), Fraction(1, 4))
aa = (Fraction(1), Fraction(-1, 2), Fraction(1, 8))
augmented = []
for row in range(size):
matrix_row = [aa[row - col] if 0 <= row - col <= 2 else Fraction()
for col in range(size)]
rhs = sum((bs[k] * xs[row - k] for k in range(3) if row >= k), Fraction())
if row < 2:
rhs += Fraction(state[row])
augmented.append(matrix_row + [rhs])
for col in range(size):
pivot = next(r for r in range(col, size) if augmented[r][col])
augmented[col], augmented[pivot] = augmented[pivot], augmented[col]
scale = augmented[col][col]
augmented[col] = [v / scale for v in augmented[col]]
for row in range(size):
if row != col:
scale = augmented[row][col]
augmented[row] = [a - scale * b for a, b in zip(augmented[row], augmented[col])]
return tuple(row[-1] for row in augmented)
def close(a, b):
return math.isclose(float(a), float(b), abs_tol=ABS, rel_tol=REL)
def sequence_close(actual, expected):
return len(actual) == len(expected) and all(close(a, b) for a, b in zip(actual, expected))
def rejected(samples, state=(0, 0), expected=(TypeError, ValueError)):
try:
biquad(samples, state)
except expected:
return True
return False
def run_checks():
impulse = (1, 0, 0, 0, 0, 0)
expected = (Fraction(1, 4), Fraction(5, 8), Fraction(17, 32),
Fraction(3, 16), Fraction(7, 256), Fraction(-5, 512))
actual, final_state = biquad(impulse)
checks = {}
checks["literal_impulse_and_final_transformed_state"] = (
sequence_close(actual, expected)
and sequence_close(final_state, (Fraction(-17, 2048), Fraction(5, 4096))))
wrong_sign_second = Fraction(1, 2) + Fraction(-1, 2) * Fraction(1, 4)
checks["feedback_sign_is_not_denominator_coefficient_addition"] = (
wrong_sign_second == Fraction(3, 8) and not close(actual[1], wrong_sign_second))
first, middle_state = biquad(impulse[:2])
continuation, _ = biquad(impulse[2:], middle_state)
reset, _ = biquad(impulse[2:])
checks["literal_block_boundary_state_and_reset_witness"] = (
sequence_close(middle_state, (Fraction(17, 32), Fraction(-5, 64)))
and close(continuation[0], Fraction(17, 32)) and reset[0] == 0
and sequence_close(first + continuation, actual))
cut_ok = True
for cut in range(len(impulse) + 1):
left, carry = biquad(impulse[:cut])
right, last = biquad(impulse[cut:], carry)
cut_ok = cut_ok and sequence_close(left + right, actual) and sequence_close(last, final_state)
checks["all_seven_continuation_cuts_inside_one_group"] = cut_ok
cases, oracle_ok, max_error = 0, True, 0.0
for size in range(5):
for samples in itertools.product((-1, 0.1, 1), repeat=size):
for state in ((0, 0), (0.125, -0.0625)):
result, _ = biquad(samples, state)
reference = global_fraction_reference(samples, state)
oracle_ok = oracle_ok and sequence_close(result, reference)
max_error = max([max_error] + [abs(x - float(y)) for x, y in zip(result, reference)])
cases += 1
checks["global_fraction_242_systems_inside_one_group"] = oracle_ok and cases == 242
explicit, _ = biquad((0, 0, 0), (0.125, -0.0625))
checks["internal_state_is_not_previous_input_history"] = sequence_close(
explicit, (Fraction(1, 8), Fraction(0), Fraction(-1, 64)))
step, _ = biquad((1,) * 32)
checks["step_transient_and_exact_profile_dc_gain"] = (
sequence_close(step[:3], (Fraction(1, 4), Fraction(7, 8), Fraction(45, 32)))
and max(step) > 1.6 and close(step[-1], Fraction(8, 5)))
tail, _ = biquad((0, 0), middle_state)
checks["no_fir_length_tail_flush_for_feedback_filter"] = (
sequence_close(tail, expected[2:4]) and tail[-1] != 0 and actual[-1] != 0)
signal = (-1, 0.5, 2, -0.25)
y, _ = biquad(signal)
twice, _ = biquad(tuple(2 * x for x in signal))
checks["zero_initial_state_linearity_for_bounded_gain"] = sequence_close(twice, tuple(2 * x for x in y))
delayed, _ = biquad((0,) + impulse)
checks["zero_initial_state_one_sample_delay"] = sequence_close(delayed, (0,) + actual)
empty, same = biquad((), (0.125, -0.0625))
checks["empty_block_preserves_internal_state_without_emission"] = empty == () and same == (0.125, -0.0625)
boundary, _ = biquad((-4, 4) * 16)
checks["inclusive_sample_range_and_32_sample_block"] = len(boundary) == 32 and all(math.isfinite(v) for v in boundary)
class FloatSubclass(float):
pass
checks["sample_container_type_length_and_value_contract"] = (
rejected([1]) and rejected((0,) * 33)
and all(rejected((v,)) for v in (True, "1", 1j, FloatSubclass(1), math.nan, math.inf, -math.inf, 4.01, 10 ** 500)))
checks["two_finite_actual_internal_states_contract"] = (
rejected((), [0, 0]) and rejected((), ()) and rejected((), (0, 0, 0))
and all(rejected((), (0, v)) for v in (True, "0", 1j, math.nan, math.inf, 10 ** 500)))
checks["finite_input_state_arithmetic_overflow_has_no_success_result"] = rejected(
(0,), (sys.float_info.max, sys.float_info.max), expected=(ArithmeticError,))
poles = (complex(0.25, 0.25), complex(0.25, -0.25))
checks["literal_poles_and_dc_transfer_arithmetic"] = (
all(p * p - 0.5 * p + 0.125 == 0 and abs(p) < 1 for p in poles)
and Fraction(1, 4) + Fraction(1, 2) + Fraction(1, 4) == 1
and Fraction(1, 1) / (1 - Fraction(1, 2) + Fraction(1, 8)) == Fraction(8, 5))
assert all(type(v) is bool and v for v in checks.values())
return {"python": platform.python_version(), "dependencies": "standard library only",
"scipy_executed": False, "matlab_executed": False,
"units": {"samples": "dimensionless synthetic amplitude", "coefficients": "dimensionless",
"internal_states": "same synthetic amplitude, not prior input samples", "index": "sample index, no seconds/sample rate input"},
"numerator": B, "denominator": A, "maximum_block_samples": 32,
"absolute_tolerance": ABS, "relative_tolerance": REL,
"impulse_input": impulse, "impulse_output": actual,
"impulse_exact_output": [str(x) for x in expected], "final_internal_state": final_state,
"state_after_two_impulse_samples": middle_state,
"continued_first_output": continuation[0], "reset_first_output": reset[0],
"explicit_state_zero_input_output": explicit,
"step_last_output": step[-1], "step_largest_output": max(step),
"theoretical_dc_gain": "8/5", "literal_poles": [[p.real, p.imag] for p in poles],
"pole_modulus": abs(poles[0]), "fraction_systems_inside_one_group": cases,
"maximum_fraction_reference_absolute_difference": max_error,
"continuation_cuts_inside_one_group": len(impulse) + 1,
"checks": checks, "unique_check_groups": len(checks), "all_passed": True,
"limits": ["Fixed coefficients, not a general design or SciPy API implementation",
"Two accumulator states are not two past inputs/outputs",
"Finite blocks and float checks are not a general numerical stability proof",
"Pole statement applies to this exact causal linear mathematical profile",
"No actual sound, model, noise removal, real-time, hardware or speech quality evaluation"]}
if __name__ == "__main__":
print(json.dumps(run_checks(), ensure_ascii=False, indent=2))
تمامِ ورودیِ قاب پیش از شروعِ محاسبه بررسی میشود. تابع با مقدارهای محلی کار میکند و در سرریز، نتیجهٔ موفقِ نیمهکاره برنمیگرداند. جمع با math.fsum است، ولی حاصلضربهای قبلی همچنان گرد میشوند و تضمینِ دقتِ نامحدود نداریم. حتی با ورودیِ صفر، دو حالتِ متناهیِ نزدیکِ بیشینهٔ float میتوانند جمعِ نامتناهی بسازند؛ این حالت صریحاً ArithmeticError میدهد.
برای پردازشِ تکهای باید دو مقدارِ برگشتیِ حالت را به فراخوانیِ بعد بدهیم. تابع خالصِ آفلاین است؛ گرفتنِ قاب از شبکه، همزمانی، زمانبندی و مهلتِ صوت را مدیریت نمیکند. اجرای موفقِ ۳۲ نمونه، توانِ پردازشِ بیدرنگ را اثبات نمیکند. SciPy، MATLAB و کتابخانهٔ صوتی در اجرای این فایل استفاده نشدهاند؛ ارجاعِ مستندات، ادعای اجرای آن ابزارها نیست.
۴. پاسخ ضربه و مرجع دستگاه مستقل

یک ورودیِ یک و سپس پنج صفر، پاسخِ دقیقِ یکچهارم، پنجهشتم، هفدهسیودوم، سهشانزدهم، هفتدویستوپنجاهوششم و منفی پنجپانصدودوازدهم میدهد. برای نمونهٔ دوم، سهمِ ورودیِ قبلی یکدوم و سهمِ بازخورد یکهشتم است؛ جمع پنجهشتم میشود. اگر علامتِ مخرج را مستقیم به خروجی اضافه کنیم، سههشتم میگیریم؛ پاسخِ دستی این اشتباه را آشکار میکند.
پس از دو ورودیِ اول، حالتِ درست هفدهسیودوم و منفی پنجشصتوچهارم است. ادامهٔ قاب با ورودیِ صفر از هفدهسیودوم آغاز میشود، اما بازنشانیِ حالت، صفر میدهد. هر هفت محلِ برشِ همین ورودی بررسی شدهاند و یک گروه هستند. برابر بودنِ طولِ خروجیِ تکهای و یکجا، بدون انتقالِ حالت، شاهدِ برابریِ عددها نیست.
مرجعِ مستقل، همهٔ خروجیهای قطعه را مجهولِ یک دستگاهِ محدود میگیرد. روی قطرِ ماتریس یک، روی قطرِ زیرین منفی یکدوم و روی قطرِ دوم یکهشتم مینشیند. سمتِ راست از ورودیها ساخته میشود و دو حالتِ اولیه به دو سطرِ نخست اضافه میشوند. حذفِ گاوسـژردن با Fraction، دستگاه را یکجا حل میکند؛ تابعِ اصلی و بهروزرسانیِ دو حالت فراخوانی نمیشوند.
{
"python": "3.14.5",
"dependencies": "standard library only",
"scipy_executed": false,
"matlab_executed": false,
"units": {
"samples": "dimensionless synthetic amplitude",
"coefficients": "dimensionless",
"internal_states": "same synthetic amplitude, not prior input samples",
"index": "sample index, no seconds/sample rate input"
},
"numerator": [
0.25,
0.5,
0.25
],
"denominator": [
1.0,
-0.5,
0.125
],
"maximum_block_samples": 32,
"absolute_tolerance": 1e-12,
"relative_tolerance": 1e-12,
"impulse_input": [
1,
0,
0,
0,
0,
0
],
"impulse_output": [
0.25,
0.625,
0.53125,
0.1875,
0.02734375,
-0.009765625
],
"impulse_exact_output": [
"1/4",
"5/8",
"17/32",
"3/16",
"7/256",
"-5/512"
],
"final_internal_state": [
-0.00830078125,
0.001220703125
],
"state_after_two_impulse_samples": [
0.53125,
-0.078125
],
"continued_first_output": 0.53125,
"reset_first_output": 0.0,
"explicit_state_zero_input_output": [
0.125,
0.0,
-0.015625
],
"step_last_output": 1.6000000000000014,
"step_largest_output": 1.62109375,
"theoretical_dc_gain": "8/5",
"literal_poles": [
[
0.25,
0.25
],
[
0.25,
-0.25
]
],
"pole_modulus": 0.3535533905932738,
"fraction_systems_inside_one_group": 242,
"maximum_fraction_reference_absolute_difference": 1.1102230246251565e-16,
"continuation_cuts_inside_one_group": 7,
"checks": {
"literal_impulse_and_final_transformed_state": true,
"feedback_sign_is_not_denominator_coefficient_addition": true,
"literal_block_boundary_state_and_reset_witness": true,
"all_seven_continuation_cuts_inside_one_group": true,
"global_fraction_242_systems_inside_one_group": true,
"internal_state_is_not_previous_input_history": true,
"step_transient_and_exact_profile_dc_gain": true,
"no_fir_length_tail_flush_for_feedback_filter": true,
"zero_initial_state_linearity_for_bounded_gain": true,
"zero_initial_state_one_sample_delay": true,
"empty_block_preserves_internal_state_without_emission": true,
"inclusive_sample_range_and_32_sample_block": true,
"sample_container_type_length_and_value_contract": true,
"two_finite_actual_internal_states_contract": true,
"finite_input_state_arithmetic_overflow_has_no_success_result": true,
"literal_poles_and_dc_transfer_arithmetic": true
},
"unique_check_groups": 16,
"all_passed": true,
"limits": [
"Fixed coefficients, not a general design or SciPy API implementation",
"Two accumulator states are not two past inputs/outputs",
"Finite blocks and float checks are not a general numerical stability proof",
"Pole statement applies to this exact causal linear mathematical profile",
"No actual sound, model, noise removal, real-time, hardware or speech quality evaluation"
]
}
شانزده گروهِ نامدار در اجرای واقعی موفق شدند. ۲۴۲ دستگاهِ کوچک، با دو حالتِ آغازین و ورودیهای محدود، فقط یک گروهِ مرجعاند. کسرِ ساختهشده از float، همان عددِ دودویی را نگه میدارد؛ مقدارِ ۰٫۱ را کسرِ دقیقِ یکدهم فرض نکردهایم. بیشینهٔ اختلاف حدود 1.11e-16 شد؛ تلورانس مطلق و نسبیِ 1e-12 برای همین دادهها انتخاب شده است.
۵. چهار پرسش و حد پایداری و صوت واقعی

مخرجِ نظری، چندجملهایِ z² - 0.5z + 0.125 میسازد. دو قطبِ آن 0.25 ± 0.25j و قدرشان حدود ۰٫۳۵۳۵۵ است. بحثِ پایداری در کتابِ جولیوس اسمیت مرزِ داخلِ دایرهٔ واحد را توضیح میدهد. این استدلال برای همین مدلِ علّیِ خطی و ضرایبِ دقیق است؛ آزمونِ محدودِ float یا اجرای MATLAB نیست و رفتارِ هر پیادهسازیِ کمدقت را تضمین نمیکند.
بهرهٔ نظری در ورودیِ ثابت، هشتپنجم است؛ نرمال بودنِ ضریبِ نخستِ مخرج، بهرهٔ واحد نمیسازد. پاسخِ پلهٔ آزمون در نمونهٔ آخرِ قابِ ۳۲ عضوی حدود ۱٫۶ شد، اما گذرای آن از این مقدار بالاتر میرود. برای تحلیلِ طیفیِ جداگانه، درسِ دامنهٔ FFT و پنجره مسئلهٔ دیگری دارد؛ از این پاسخِ کوتاه، نمودارِ کیفیتِ گفتار یا نتیجهٔ حذف نویز استخراج نمیکنیم.
پرسش اول: چرا ضریبِ منفی یکدوم، سهمِ مثبت میسازد؟ پاسخ: معادله سهمِ مخرج را کم میکند؛ منفیِ منفی یکدوم، مثبت یکدوم است. آزمونِ نمونهٔ دوم، پنجهشتم را در برابر پاسخِ غلطِ سههشتم نگه میدارد؛ تغییرِ علامت را با عوضکردنِ بیدلیلِ انتظار پنهان نمیکنیم.
پرسش دوم: چرا دو ورودیِ قبلی را جای حالت نگذاریم؟ پاسخ: حالتها انباشتگرهای ساختارِ تبدیلشدهاند. پس از دو نمونهٔ ضربه، مقدارشان هفدهسیودوم و منفی پنجشصتوچهارم است، نه یک و صفر. تبدیلِ نمایشِ تاریخچه به این حالتها، رابطه و آزمونِ جدا میخواهد.
پرسش سوم: دو صفرِ اضافی پاسخ را تمام میکنند؟ پاسخ: خیر؛ پس از همان مرزِ دو نمونه، دو خروجیِ بعدی هفدهسیودوم و سهشانزدهماند و صفر نیستند. در این پروفایلِ IIR طولِ متناهیِ ثابتی برای تخلیهٔ کامل وعده نمیدهیم؛ پایانِ داده، سیاستِ توقفِ دریافت است.
پرسش چهارم: قطبهای داخلِ دایره یعنی صوت بهتر شده است؟ پاسخ: خیر؛ پایداریِ مدلِ نظری، طبیعی بودنِ گفتار، مناسب بودنِ پاسخ فرکانسی یا رعایتِ موعدِ اجرا را ارزیابی نمیکند. منابع در ۲۶ مهر ۱۴۰۵ خوانده شدند؛ نسخهٔ SciPy 1.18.0، بخشِ کتابِ اسمیت و تاریخِ یادداشتِ W3C از اجرای محلی جدا ثبتاند. مطلب آموزش پایه است، نه خبرِ تازه یا گزارشِ محصول.
منابع و مطالعهٔ بیشتر
- SciPy 1.18.0؛ معادلهٔ تفاضلی و ساختار direct-II transposed
- W3C Audio EQ Cookbook؛ تابع تبدیل و علامت بازخورد
- جولیوس اسمیت؛ پایداری و قطبهای تابع تبدیلِ علّی
بررسی منابع: ۲۶ مهر ۱۴۰۵. تحلیلها و پیشنهادهای اجرایی، جمعبندی نویسندهاند.

