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

دو ورودی، عددهای صحیحِ علامتدارِ منفیِ ۳۲۷۶۸ تا مثبتِ ۳۲۷۶۷ هستند. مقدارِ هر ورودی را کُد تقسیمبر ۳۲۷۶۸ تعریف میکنیم؛ این همان قراردادِ کسریِ علامتدارِ ۱٫۱۵ در این درس است. کُدِ ۱۶۳۸۴، مقدارِ یکدوم و کُدِ منفیِ ۳۲۷۶۸، مقدارِ منفیِ یک دارد. کُد و مقدارِ بازشده، هر دو بیواحدند؛ ولتاژ، احتمال یا سطحِ صدای کالیبرهشده نیستند.
فاصلهٔ دو مقدارِ مجاور یکسیودوهزاروهفتصدوشصتوهشتم است. بازهٔ بازسازی از منفیِ یک تا یک منهای همین فاصله میرسد؛ مثبتِ یک در آن نمایش ندارد. بنابراین نامِ «شانزدهبیتی» بهتنهایی قرارداد را تعیین نمیکند. رمزگشاییِ PCMU به ظرفِ عددیِ خطی نگاشتِ دیگری داشت؛ خروجیِ آن را بدونِ تصمیم دربارهٔ مقیاس، خودکار ورودیِ Q15 نمینامیم.
مستنداتِ ضربِ برداریِ CMSIS-DSP، ضربِ عضوهای متناظر و اشباعِ Q15 را معرفی میکند. برنامهٔ حاضر تنها دو اسکالر میگیرد؛ ترتیبِ نمونهها، طولِ بافر، تبدیلِ شناور به Q15 و قراردادِ کانال را پیادهسازی نمیکند. نوعِ ورودی باید دقیقاً int باشد؛ bool، شناور، کسر، رشته و زیرکلاس نیز پذیرفته نیستند.
۲. ضربِ پهنتر و اشباعِ مرزِ مثبت

حاصلِ صحیحِ دو کُد، پیش از کاهشِ مقیاس نگه داشته میشود. برای دو کُدِ ۱۶۳۸۴، این حاصل ۲۶۸۴۳۵۴۵۶ است. چون هر ورودی پانزده بیتِ کسری دارد، حاصل در مقیاسِ عددیِ اولیه سی بیتِ کسری دارد. برای برگشت به پانزده بیتِ کسری، حاصل را بر ۳۲۷۶۸ با سیاستِ رو به پایین تقسیم میکنیم؛ کُدِ خروجی ۸۱۹۲ و مقدارِ بازشده یکچهارم میشود.
اگر پیش از این تقسیم فقط شانزده بیتِ کمارزشِ حاصلِ مثال را نگه داریم، صفر میماند و اطلاعات از دست میرود. پهنکردنِ عدد پس از حذفِ بیتها آن را ترمیم نمیکند. در دامنهٔ دو ورودیِ حاضر، حاصلِ صحیح بین منفیِ ۱۰۷۳۷۰۹۰۵۶ و مثبتِ ۱۰۷۳۷۴۱۸۲۴ است و در بازهٔ علامتدارِ سیودوبیتی جا میگیرد. پایتون محاسبهٔ صحیحِ خودش را انجام میدهد؛ سرریزِ ثباتِ سختافزار شبیهسازی نشده است.
استثنای مهم، ضربِ دو کُدِ منفیِ ۳۲۷۶۸ است: مقدارِ دقیق مثبتِ یک، اما کُدِ پس از تقسیم ۳۲۷۶۸ میشود. خروجی به ۳۲۷۶۷ محدود میشود و مقدارِ بازشده یک گام کمتر از یک است. منبعِ ثابتِ تابعِ arm_mult_q15 در شاخهٔ اسکالر، حاصلِ پهنتر، جابهجاییِ پانزدهبیتی و اشباعِ شانزدهبیتی را نشان میدهد. شاخهٔ MVE دستورِ برداریِ جدا دارد؛ آن را اجرا یا معادلسازیِ تجربی نکردهایم.
در سویِ منفی، کوچکترین حاصلِ همین دامنه از منفیِ یک ضربدر بزرگترین مقدارِ مثبت میآید و کُدِ منفیِ ۳۲۷۶۷ میدهد. بنابراین با اینکه منفیِ ۳۲۷۶۸ ورودیِ معتبر است، خروجیِ این ضربِ خاص به آن نمیرسد. تنها گوشهٔ نیازمندِ محدودسازی، همان دو ورودیِ منفیِ یک است؛ این نتیجه را به جمع، بایاس یا قالبهای دیگر منتقل نمیکنیم.
۳. برشِ رو به پایین را با گردکردن اشتباه نگیریم

«حذفِ بیتهای کسری» برای عددِ منفی، الزاماً حرکت به سوی صفر نیست. بخشِ جابهجاییِ بیت در مرجعِ پایتون، جابهجاییِ راستِ عددِ صحیح را تقسیمِ رو به پایین تعریف میکند. پس منفیِ یک ضربدر ۱۶۳۸۴، پیش از انتخابِ کُد، منفیِ نیمِ واحدِ کُد است و به منفیِ یک میرسد؛ تبدیلِ مستقیمِ همین کسر با int به صفر میرسد و سیاستِ دیگری است.
شاهدِ مثبتِ متناظر به صفر میرسد. بنابراین با برعکسکردنِ علامتِ فقط یک ورودی، نباید همیشه منفیِ کُدِ قبلی را انتظار داشت. نزدیکترینگردکردن و شیوهٔ شکستنِ تساویِ نیمگام هم قراردادهای جدا هستند؛ تابعِ این درس هیچ گزینهای برای انتخابِ آنها ندارد. این تعریفِ روشنِ پایتون را نیز به همهٔ کامپایلرها، عملگرهای سی و شاخههای سختافزاری تعمیم نمیدهیم.
بدونِ اشباع، مقدارِ بازشده منهای حاصلِ دقیق، از منفیِ یک گام بیشتر و حداکثر صفر است. دلیلش این است که باقیماندهٔ تقسیمِ رو به پایین از صفر تا ۳۲۷۶۷ میماند. در گوشهٔ مثبتِ اشباعشده، خطا دقیقاً منفیِ یک گام میشود. این کران دربارهٔ همین دو مقدارِ بازسازیشده است؛ خطایِ تبدیلِ سیگنالِ اولیه، انباشتِ چند مرحله و کیفیتِ شنیداری را پوشش نمیدهد.
۴. کدِ کامل و مرجعِ گویای مستقل

کدِ کامل با Python 3.14.5 و کتابخانهٔ استاندارد یک بار اجرا شده است. تابعِ اصلی ضربِ صحیح و جابهجایی را انجام میدهد. مرجع، هر ورودی را جدا به Fraction باز میکند و با جستوجوی دودویی، بزرگترین نقطهٔ شبکهٔ قابلنمایش را که از حاصلِ دقیق بیشتر نیست مییابد. مرجع از تابعِ اصلی، جابهجاییِ حاصل یا نسخهٔ دوبارهٔ شاخههای اشباع کمک نمیگیرد.
مقایسههای مرجع کسری و دقیقاند؛ به float یا تلورانس تکیه ندارند. وقتی حاصلِ دقیق یک باشد، بزرگترین نقطهٔ مجازِ شبکه هنوز از آن کوچکتر است و جستوجو به همان انتهای مثبت میرسد. کرانِ پایینِ حاصل نیز تضمین میکند دستکم یک نقطهٔ مجاز برای مقایسه وجود دارد؛ مرجع برای ورودیِ دلخواهِ بیرونِ این قرارداد معرفی نشده است.
"""Own signed1.15 scalar floor/saturation contract; no CMSIS/hardware execution."""
from fractions import Fraction
from itertools import product
import json
import platform
import random
SCALE = 1 << 15
LOW, HIGH = -SCALE, SCALE - 1
def _signed16(value, name):
if type(value) is not int or not LOW <= value <= HIGH:
raise ValueError(name + "_contract")
return value
def mul_q15(a, b):
"""Two exact signed16 codes, interpreted as dimensionless code/32768."""
a, b = _signed16(a, "a"), _signed16(b, "b")
wide = a * b # Python integer: do not narrow the product to signed16.
shifted = wide >> 15 # This Python integer operation is floor division.
return min(HIGH, max(LOW, shifted))
def explain_pair(a, b):
code = mul_q15(a, b)
wide = a * b
exact = Fraction(a, SCALE) * Fraction(b, SCALE)
decoded = Fraction(code, SCALE)
return {"input_codes": (a, b), "input_values": (Fraction(a, SCALE), Fraction(b, SCALE)),
"wide_integer_product": wide, "unclamped_shifted_code": wide >> 15,
"output_code": code, "output_value": decoded,
"exact_value_product": exact, "decoded_minus_exact": decoded - exact,
"saturated": code != wide >> 15}
def fraction_grid_oracle(a, b):
"""Valid fixtures only: largest representable grid value <= exact product.
Binary-search the rational grid; no production call, product right shift,
fixed32768 integer division, or copied saturation branches.
"""
target = Fraction(a, SCALE) * Fraction(b, SCALE)
low, high, best = LOW, HIGH, LOW
while low <= high:
middle = (low + high) // 2
if Fraction(middle, SCALE) <= target:
best = middle
low = middle + 1
else:
high = middle - 1
return best
def rejected(call, message):
try:
call()
except ValueError as error:
return str(error) == message
return False
def run_example():
checks = {}
def check(name, value):
assert type(value) is bool and value, name
checks[name] = value
check("literal_quarter_products_and_signed_code_value_separation",
mul_q15(16384, 16384) == 8192
and mul_q15(-16384, 16384) == -8192
and mul_q15(-16384, -16384) == 8192
and Fraction(8192, SCALE) == Fraction(1, 4))
endpoint = explain_pair(LOW, LOW)
check("minus_one_times_minus_one_saturates_unrepresentable_positive_one",
endpoint["exact_value_product"] == 1
and endpoint["wide_integer_product"] == 2 ** 30
and endpoint["unclamped_shifted_code"] == SCALE
and endpoint["output_code"] == HIGH and endpoint["saturated"] is True
and mul_q15(LOW, HIGH) == -HIGH)
check("largest_positive_Q15_is_not_a_positive_one_identity",
mul_q15(HIGH, HIGH) == 32766
and mul_q15(HIGH, 16384) == 16383
and rejected(lambda: mul_q15(SCALE, 0), "a_contract"))
check("positive_and_negative_half_code_ties_follow_floor_not_truncation_or_nearest_even",
mul_q15(1, 16384) == 0 and mul_q15(-1, 16384) == -1
and int(Fraction(-1, 2)) == 0 and round(Fraction(-1, 2)) == 0
and mul_q15(-1, 1) == -1 and mul_q15(1, 1) == 0)
corners = [a * b for a, b in product((LOW, HIGH), repeat=2)]
wrong_narrow_product = (16384 * 16384) & 0xffff
check("wide_product_fits_signed32_but_early_signed16_narrowing_loses_information",
min(corners) == -1073709056 and max(corners) == 1073741824
and -(2 ** 31) <= min(corners) <= max(corners) < 2 ** 31
and wrong_narrow_product == 0 and mul_q15(16384, 16384) != 0)
axis_ok = all(mul_q15(a, 0) == 0
and mul_q15(a, LOW) == (HIGH if a == LOW else -a)
for a in range(LOW, HIGH + 1))
check("all_65536_codes_on_zero_and_negative_one_axes_one_family", axis_ok)
grid = (-32768, -32767, -24577, -16385, -16384, -1, 0, 1,
3, 16383, 16384, 16385, 24576, 32766, 32767)
check("operand_swap_and_joint_sign_change_preserve_product_when_inputs_remain_valid",
all(mul_q15(a, b) == mul_q15(b, a) for a, b in product(grid, repeat=2))
and all(mul_q15(a, b) == mul_q15(-a, -b)
for a, b in product(grid[1:], repeat=2)))
rng = random.Random(160019)
pairs = list(product(grid, repeat=2))
pairs += [(rng.randint(LOW, HIGH), rng.randint(LOW, HIGH)) for _ in range(1024)]
check("1249_directed_and_seeded_pairs_match_independent_Fraction_grid_search_one_family",
len(pairs) == 1249 and all(mul_q15(a, b) == fraction_grid_oracle(a, b)
for a, b in pairs))
error_ok = True
for a, b in pairs:
info = explain_pair(a, b)
residue = a * b - info["unclamped_shifted_code"] * SCALE
error = info["decoded_minus_exact"]
error_ok &= 0 <= residue < SCALE and -Fraction(1, SCALE) <= error <= 0
error_ok &= (info["saturated"] == (a == b == LOW))
error_ok &= (error == -Fraction(1, SCALE) if info["saturated"]
else error > -Fraction(1, SCALE))
error_ok &= -HIGH <= info["output_code"] <= HIGH
check("same_sample_family_checks_floor_residue_error_interval_and_unique_saturation_corner",
bool(error_ok))
monotone = all(mul_q15(12345, b) <= mul_q15(12345, b + 1)
and mul_q15(-12345, b) >= mul_q15(-12345, b + 1)
for b in range(LOW, HIGH))
check("fixed_positive_or_negative_operand_has_expected_monotone_direction_on_all_65536_codes",
monotone)
left = mul_q15(mul_q15(3, 20000), 20000)
right = mul_q15(3, mul_q15(20000, 20000))
check("intermediate_quantization_makes_this_multiplication_nonassociative",
(left, right) == (0, 1))
class IntSubclass(int):
pass
invalid = (True, False, 1.0, "1", None, Fraction(1), 1j,
IntSubclass(1), LOW - 1, HIGH + 1, 10 ** 80)
check("left_operand_exact_int_type_and_signed16_bounds_are_enforced",
all(rejected(lambda value=value: mul_q15(value, 0), "a_contract") for value in invalid))
check("right_operand_exact_int_type_and_signed16_bounds_are_enforced",
all(rejected(lambda value=value: mul_q15(0, value), "b_contract") for value in invalid))
fresh = explain_pair(16384, 16384)
fresh["output_code"] = -100
check("scalar_result_is_an_int_and_explanation_records_do_not_share_mutable_state",
type(mul_q15(0, 0)) is int and explain_pair(16384, 16384)["output_code"] == 8192)
assert len(checks) == 14
fixtures = ((16384, 16384), (1, 16384), (-1, 16384),
(LOW, LOW), (HIGH, HIGH), (LOW, HIGH))
return {"python": platform.python_version(), "dependencies": "standard library only",
"units": {"input_and_output_codes": "dimensionless signed integers",
"decoded_values_and_product": "dimensionless; code/32768",
"one_output_grid_step": str(Fraction(1, SCALE))},
"input_code_range": (LOW, HIGH), "possible_output_code_range": (-HIGH, HIGH),
"fixtures": [explain_pair(a, b) for a, b in fixtures],
"nonassociative_witness": {"input_codes": (3, 20000, 20000),
"left_grouping_code": left, "right_grouping_code": right},
"oracle_pairs_inside_one_family": len(pairs),
"axis_codes_inside_one_family": 65536,
"monotone_adjacent_pairs_inside_one_family": 65535,
"checks": checks,
"limits": ["Own scalar floor-plus-saturation policy, not every rounding convention.",
"No float-to-Q15 conversion, vector buffer, PCM codec, model or audio chain executed.",
"No C compiler, CMSIS scalar/MVE branch, intrinsic or target hardware executed.",
"No timing, energy, memory savings, signal quality or model accuracy measurement."]}
if __name__ == "__main__":
print(json.dumps(run_example(), default=str, ensure_ascii=False, indent=2))
۲۲۵ زوجِ جهتدار و ۱۰۲۴ زوج با بذرِ ثابت، در مجموع ۱۲۴۹ زوجِ مرجع و فقط یک گروهِ نامدار هستند. همهٔ ۶۵۵۳۶ کُد روی محورِ ضرب در صفر و منفیِ یک نیز یک خانوادهاند؛ بررسیِ جهتِ یکنواختی برای دو ضریبِ ثابت، خانوادهٔ دیگری است. این پوشش، بررسیِ تمامِ بیش از چهار میلیارد زوجِ ممکن نیست. در مجموع چهارده گروه موفق شدهاند و اجرای مجدد گروهِ تازه نمیسازد.
{
"python": "3.14.5",
"dependencies": "standard library only",
"units": {
"input_and_output_codes": "dimensionless signed integers",
"decoded_values_and_product": "dimensionless; code/32768",
"one_output_grid_step": "1/32768"
},
"input_code_range": [
-32768,
32767
],
"possible_output_code_range": [
-32767,
32767
],
"fixtures": [
{
"input_codes": [
16384,
16384
],
"input_values": [
"1/2",
"1/2"
],
"wide_integer_product": 268435456,
"unclamped_shifted_code": 8192,
"output_code": 8192,
"output_value": "1/4",
"exact_value_product": "1/4",
"decoded_minus_exact": "0",
"saturated": false
},
{
"input_codes": [
1,
16384
],
"input_values": [
"1/32768",
"1/2"
],
"wide_integer_product": 16384,
"unclamped_shifted_code": 0,
"output_code": 0,
"output_value": "0",
"exact_value_product": "1/65536",
"decoded_minus_exact": "-1/65536",
"saturated": false
},
{
"input_codes": [
-1,
16384
],
"input_values": [
"-1/32768",
"1/2"
],
"wide_integer_product": -16384,
"unclamped_shifted_code": -1,
"output_code": -1,
"output_value": "-1/32768",
"exact_value_product": "-1/65536",
"decoded_minus_exact": "-1/65536",
"saturated": false
},
{
"input_codes": [
-32768,
-32768
],
"input_values": [
"-1",
"-1"
],
"wide_integer_product": 1073741824,
"unclamped_shifted_code": 32768,
"output_code": 32767,
"output_value": "32767/32768",
"exact_value_product": "1",
"decoded_minus_exact": "-1/32768",
"saturated": true
},
{
"input_codes": [
32767,
32767
],
"input_values": [
"32767/32768",
"32767/32768"
],
"wide_integer_product": 1073676289,
"unclamped_shifted_code": 32766,
"output_code": 32766,
"output_value": "16383/16384",
"exact_value_product": "1073676289/1073741824",
"decoded_minus_exact": "-1/1073741824",
"saturated": false
},
{
"input_codes": [
-32768,
32767
],
"input_values": [
"-1",
"32767/32768"
],
"wide_integer_product": -1073709056,
"unclamped_shifted_code": -32767,
"output_code": -32767,
"output_value": "-32767/32768",
"exact_value_product": "-32767/32768",
"decoded_minus_exact": "0",
"saturated": false
}
],
"nonassociative_witness": {
"input_codes": [
3,
20000,
20000
],
"left_grouping_code": 0,
"right_grouping_code": 1
},
"oracle_pairs_inside_one_family": 1249,
"axis_codes_inside_one_family": 65536,
"monotone_adjacent_pairs_inside_one_family": 65535,
"checks": {
"literal_quarter_products_and_signed_code_value_separation": true,
"minus_one_times_minus_one_saturates_unrepresentable_positive_one": true,
"largest_positive_Q15_is_not_a_positive_one_identity": true,
"positive_and_negative_half_code_ties_follow_floor_not_truncation_or_nearest_even": true,
"wide_product_fits_signed32_but_early_signed16_narrowing_loses_information": true,
"all_65536_codes_on_zero_and_negative_one_axes_one_family": true,
"operand_swap_and_joint_sign_change_preserve_product_when_inputs_remain_valid": true,
"1249_directed_and_seeded_pairs_match_independent_Fraction_grid_search_one_family": true,
"same_sample_family_checks_floor_residue_error_interval_and_unique_saturation_corner": true,
"fixed_positive_or_negative_operand_has_expected_monotone_direction_on_all_65536_codes": true,
"intermediate_quantization_makes_this_multiplication_nonassociative": true,
"left_operand_exact_int_type_and_signed16_bounds_are_enforced": true,
"right_operand_exact_int_type_and_signed16_bounds_are_enforced": true,
"scalar_result_is_an_int_and_explanation_records_do_not_share_mutable_state": true
},
"limits": [
"Own scalar floor-plus-saturation policy, not every rounding convention.",
"No float-to-Q15 conversion, vector buffer, PCM codec, model or audio chain executed.",
"No C compiler, CMSIS scalar/MVE branch, intrinsic or target hardware executed.",
"No timing, energy, memory savings, signal quality or model accuracy measurement."
]
}
در ضربِ داخلیِ هشتبیتی با نقطهٔ صفر، خروجی دوباره به کُدِ محدود برنمیگشت. اینجا همین انتخابِ شبکه و اشباع، موضوعِ اصلی است. برای سه کُدِ سه، ۲۰۰۰۰ و ۲۰۰۰۰، گروهبندیِ چپ کُدِ صفر و گروهبندیِ راست کُدِ یک میدهد؛ پس تغییرِ ترتیبِ پرانتزها پس از کوانتیزهسازیِ میانی الزاماً بیاثر نیست.
۵. چهار پرسش دربارهٔ قرارداد و محدودیتها

پرسشِ اول: چرا منفیِ یک ضربدر منفیِ یک، خروجیِ دقیقِ یک ندارد؟ پاسخ: مقدارِ ریاضی یک است، اما کُدِ لازم ۳۲۷۶۸ از حدِ مثبت بیرون است. اشباع کُدِ ۳۲۷۶۷ میدهد؛ این تصمیمِ نمایش است، نه تغییرِ قانونِ ضربِ حقیقی یا افزایشِ دقتِ داده.
پرسشِ دوم: چرا برای منفیِ نیمِ واحدِ کُد، صفر نگیریم؟ پاسخ: چون قراردادِ این تابع رو به پایین است و پاسخ منفیِ یک است. صفر گرفتن با int یا نزدیکترینگردکردنِ تساوی به زوج، تابعِ دیگری میسازد. برچسبِ روش را باید همراهِ آزمون و دادهٔ مرزی نگه داشت.
پرسشِ سوم: میتوان ۳۲۷۶۷ را ضریبِ مثبتِ یک گرفت؟ پاسخ: خیر؛ مقدارش یک منهای یک گام است. ضربِ آن در کُدِ ۱۶۳۸۴، کُدِ ۱۶۳۸۳ میدهد؛ مربعِ خودش نیز ۳۲۷۶۶ است. جایگزینکردنِ آن با همانی، حتی پیش از بحثِ کیفیتِ مدل، حساب را عوض میکند.
پرسشِ چهارم: آیا توافقِ مرجع، اجرای CMSIS روی برد را تأیید میکند؟ پاسخ: خیر؛ هیچ سی، دستورِ برداری، بافر یا بردی اجرا نشده است. نسخهٔ منبع، تنظیماتِ ساخت، حافظه و رفتارِ شاخهٔ هدف آزمایشِ جدا میخواهند. اینجا دربارهٔ تأخیر، انرژی، حافظه یا دقتِ مدل نتیجهای نداریم.
منابع در ۲۷ مهر ۱۴۰۵ خوانده شدند. منبعِ سی به بازبینیِ ثابتِ ۲۲ اکتبرِ ۲۰۲۵ متصل است؛ تاریخِ سرصفحهٔ ۲۳ آوریلِ ۲۰۲۱ و عبارتِ V1.9.0 را نسخهٔ فعلیِ کلِ کتابخانه نمینامیم. صفحاتِ main تاریخِ انتشارِ اولیهٔ مشخص ندارند؛ مرجعِ پایتون 3.14.8 نیز از اجرای واقعیِ 3.14.5 جدا ثبت شده است.
منابع و مطالعهٔ بیشتر
- CMSIS-DSP؛ منبعِ ثابتِ کاملِ arm_mult_q15 و شاخههای آن
- CMSIS-DSP؛ قراردادِ ضربِ برداری و اشباعِ Q15
- Python3.14.8؛ تعریفِ جابهجاییِ راست و تقسیمِ رو به پایین
بررسی منابع: ۲۷ مهر ۱۴۰۵. تحلیلها و پیشنهادهای اجرایی، جمعبندی نویسندهاند.

