فهرست مقاله
۱. بایتِ PCMU را PCM خطی نخوانیم

در مسیر پردازشِ صوتِ تلفن، دیدنِ یک بایت برای هر نمونه کافی نیست تا بتوانیم آن بایت را دامنهٔ خطی بخوانیم. اگر قراردادِ ورودی PCMU باشد، عددِ ذخیرهشده نشانی از یک سطحِ بازسازی در قانونِ مو است. این درس دقیقاً همان تبدیلِ کوچک را با پایتون میسازد؛ دادهها ساختگیاند و صدای تماس دریافت یا پخش نمیشود.
در بخشِ PCMA و PCMU در RFC3551، PCMU با قانونِ مو و هشت بیت برای هر نمونه معرفی میشود. PCMA قانونِ دیگری دارد. تابعِ این مقاله از روی خودِ بایتها تشخیص نمیدهد کدام قانون استفاده شده است؛ انتخابِ درستِ قالب باید پیش از فراخوانی، از قراردادِ معتبرِ داده معلوم باشد.
در حسابِ بستهبندیِ G.711 و بودجهٔ IP، اندازهٔ نمونهٔ از پیش کدشده را داشتیم و تعدادِ بایتهای محموله و سرآیند را حساب میکردیم. اینجا پرسش عوض میشود: هر کدام از آن بایتها به چه عددِ دامنهای باز میشود؟ شمارِ بستهها یا نرخِ شبکه از این تابع به دست نمیآید.
همچنین درسِ PCM16 و ظرفِ WAV، نمونهٔ خطی را در فایل میگذاشت. خروجیِ حاضر میتواند مادهٔ اولیهٔ یک مرحلهٔ مشابه باشد، اما بایتِ خامِ شانزدهبیتی هنوز فایلِ WAV نیست. نرخِ نمونهبرداری، کانالها و سرآیندِ فایل در این مثال ساخته نمیشوند؛ تبدیلِ دامنه بهتنهایی قراردادِ زمانی را تعیین نمیکند.
فایدهٔ محدودکردنِ مسئله این است که میتوان تمامِ فضای ورودیِ تکنمونه را بررسی کرد. فقط ۲۵۶ بایتِ ممکن داریم. در برابرِ این پوششِ کاملِ دامنهٔ گسسته، هیچ ادعایی دربارهٔ فهمِ گفتار، کیفیتِ شنیداری، سازگاریِ یک سرویس یا سرعتِ پردازش مطرح نمیکنیم. اینها پرسشهایی جدا با داده و آزمایشِ مناسبِ خودشان هستند.
۲. علامت و پلهٔ دامنه را از قرارداد بخوانیم

مرجعِ اصلیِ بازسازی، توصیهٔ G.711، بخشِ ۳٫۶ و جدولهای ۲الف و ۲ب است. اعدادِ جدول در مقیاسِ یکنواختِ چهاردهبیتی تعریف شدهاند. ما مقدارِ بازسازیِ همان جدول را در چهار ضرب میکنیم و در ظرفِ عددِ صحیحِ علامتدارِ شانزدهبیتی میگذاریم. این انتخابِ نمایش باید همراهِ داده منتقل شود؛ عددها خودشان واحدِ ولت یا فشارِ صوت ندارند.
در نتیجه، دو سرِ بازسازیِ عادیِ جدول، مثبت و منفیِ ۸۰۳۱، در قراردادِ ما به مثبت و منفیِ ۳۲۱۲۴ تبدیل میشوند. «ظرفِ شانزدهبیتی» به معنیِ تولیدِ دقتِ تازه یا رسیدن به تمامِ مقادیرِ آن ظرف نیست. کمینهٔ عمومیِ عددِ صحیحِ شانزدهبیتی منفیِ ۳۲۷۶۸ است، ولی هیچ بایتِ PCMU در این نگاشت به آن نمیرسد.
برای محاسبه، ابتدا بیتهای بایت را معکوس میکنیم. سپس بخشِ علامت، شمارهٔ قطعه و جایگاهِ داخلِ قطعه جدا میشوند. عبارتِ حسابیِ کد، مقدارِ چهاردهبیتی را میسازد و علامت را اعمال میکند. اندازهٔ گامِ بازسازی در قطعههای مختلف یکسان نیست؛ بنابراین تفاضلِ دو کدِ مجاور را نباید همیشه اختلافِ دامنهٔ یکسان دانست.
دو بایتِ هگزادسیمالِ FF و 7F هر دو به عددِ صحیحِ صفر میرسند. در این خروجیِ صحیح، صفرِ مثبت و منفی را جدا ذخیره نمیکنیم. همین برخورد نشان میدهد که حتی بازگرداندنِ عددِ بازسازی به کدِ اولیه، بدونِ قراردادِ اضافی یکتا نیست. رمزگذار نیز در برنامه وجود ندارد و هیچ بازگشتِ بدونِ اتلافی ادعا نمیشود.
نگاشتِ ما حالتِ عادیِ جدول است. بندِ ۳٫۲ استاندارد، برای شبکههایی که حذفِ نویسهٔ تمامصفر لازم دارند، جانشینیِ ویژهای توضیح میدهد. آن سیاست را اعمال نمیکنیم: بایتِ 00 اینجا سکوت نیست و به منفیِ ۳۲۱۲۴ باز میشود. فرضکردنِ صفر بودنِ همهٔ بایتهای صفر میتواند تفسیرِ دامنه را کاملاً عوض کند.
برای مقایسهٔ مقیاس، تابعِ گسترشِ قانونِ مو در انتشارِ ۲۰۲۴ بستهٔ رسمیِ G.191 نیز خوانده شده است. این نسخه در زمانِ مرور منسوخشده بود؛ آن را جدیدترین انتشار معرفی نمیکنیم. کدِ سیِ مرجع اجرا نشده و مقاله ادعای تطابقِ اجرایی با همهٔ کتابخانهها ندارد.
۳. کدِ کاملِ رمزگشای محدود

رابطِ تکنمونه فقط عددِ صحیحِ دقیقِ پایتون بین صفر و ۲۵۵ را میپذیرد. مقدارِ بولی، اعشاری، رشته و زیرکلاسِ عدد رد میشود. رابطِ جریان نیز فقط شیءِ دقیقِ bytes میگیرد؛ فهرست و حافظهٔ تغییرپذیر را خودکار تبدیل نمیکند. این سختگیری قراردادِ آموزشیِ ماست و ادعای الزامِ استاندارد برای رابطِ پایتون نیست.
سقفِ جریان ۴۰۹۶ نمونه است و ورودیِ خالی مجاز میماند. سقف برای محدود نگهداشتنِ تمرین انتخاب شده، نه به عنوانِ حدِ G.711 یا اندازهٔ مجازِ بستهٔ RTP. تابع یک نمونه برای هر بایت و به همان ترتیب میدهد؛ هیچ نمونهای برای رسیدن به طولِ دلخواه حذف یا اضافه نمیشود.
تابعِ بستهبندی، اعدادِ بازشده را با ترتیبِ بایتِ صریح به ظرفِ خامِ علامتدار تبدیل میکند. انتخابِ پیشفرض کوچکانتهاست و بزرگانتها نیز جدا آزموده میشود. این انتخاب مربوط به خروجیِ دوبایتی است؛ نباید آن را با ترتیبِ قطعهها یا معنای بیتِ علامت در بایتِ ورودی یکی گرفت.
"""Ordinary PCMU octets to a chosen signed linear16 integer convention.
G.711 table 2 normalized reconstruction values are multiplied by four.
This is not an encoder, audio player, WAV/RTP parser, or quality test.
Optional all-zero-character suppression is not applied. No third-party imports.
"""
import json
import platform
MAX_SAMPLES = 4096 # Teaching API limit, not a G.711 or RTP limit.
def decode_octet(octet):
"""Return four times the ordinary table-2 normalized mu-law value."""
if type(octet) is not int:
raise TypeError("octet must be an exact int, excluding bool")
if not 0 <= octet <= 255:
raise ValueError("octet outside 0..255")
inverted = octet ^ 0xff
segment = (inverted >> 4) & 7
position = inverted & 15
magnitude14 = ((2 * position + 33) << segment) - 33
return 4 * (-magnitude14 if inverted & 0x80 else magnitude14)
def decode_pcmu(payload):
"""Exact bytes only; caller already knows the encoding is ordinary PCMU."""
if type(payload) is not bytes:
raise TypeError("payload must be exact bytes")
if len(payload) > MAX_SAMPLES:
raise ValueError("payload exceeds the teaching sample cap")
return tuple(decode_octet(octet) for octet in payload)
def pack_linear16(payload, byteorder="little"):
"""A raw signed16 container only, with explicit byte order and no header."""
if type(byteorder) is not str:
raise TypeError("byteorder must be exact str")
if byteorder not in ("little", "big"):
raise ValueError("byteorder must be little or big")
return b"".join(sample.to_bytes(2, byteorder, signed=True)
for sample in decode_pcmu(payload))
def table_reference():
"""Independent segment-table construction in normalized14 units.
No call to decode_octet, bit inversion, exponent extraction, or bias formula.
The wire order descends separately within each polarity half.
"""
bases14 = (0, 33, 99, 231, 495, 1023, 2079, 4191)
steps14 = (2, 4, 8, 16, 32, 64, 128, 256)
levels14 = [base + position * step
for base, step in zip(bases14, steps14)
for position in range(16)]
reference = {}
for wire, level in zip(range(255, 127, -1), levels14):
reference[wire] = 4 * level
for wire, level in zip(range(127, -1, -1), levels14):
reference[wire] = -4 * level
return reference
def must_raise(exception, function, *args):
try:
function(*args)
except exception:
return True
raise AssertionError("expected exception was not raised")
def signed16_weight_reference(pair, byteorder):
"""Explicit bit weights, without int.from_bytes or production decoding."""
ordered = pair if byteorder == "big" else pair[::-1]
bits = "".join(format(octet, "08b") for octet in ordered)
return (-32768 * int(bits[0])
+ sum(int(bit) * 2 ** (14 - index)
for index, bit in enumerate(bits[1:])))
def main():
checks = {}
def check(name, condition):
assert type(condition) is bool and condition, name
checks[name] = condition
fixture = bytes.fromhex("ff7ffe7e8000ef6f")
expected_fixture = (0, 0, 8, -8, 32124, -32124, 132, -132)
directed = {0xff: 0, 0x7f: 0, 0xfe: 8, 0x7e: -8,
0xef: 132, 0x6f: -132, 0x80: 32124, 0x00: -32124}
check("directed_table_zero_low_step_and_extreme_vectors",
all(decode_octet(wire) == value for wire, value in directed.items()))
reference = table_reference()
values = decode_pcmu(bytes(range(256)))
check("all_256_octets_match_independent_normalized14_segment_table",
len(reference) == 256
and all(values[wire] == reference[wire] for wire in range(256)))
check("two_wire_zeros_and_exactly_255_distinct_reconstruction_values",
[wire for wire, value in enumerate(values) if value == 0] == [127, 255]
and len(set(values)) == 255)
check("flipping_wire_sign_bit_negates_every_reconstruction",
all(values[wire ^ 0x80] == -values[wire] for wire in range(128)))
positive = [decode_octet(wire) for wire in range(255, 127, -1)]
steps16 = (8, 16, 32, 64, 128, 256, 512, 1024)
check("positive_levels_increase_with_declared_within_segment_spacing",
all(a < b for a, b in zip(positive, positive[1:]))
and all(positive[16 * segment + j + 1]
- positive[16 * segment + j] == steps16[segment]
for segment in range(8) for j in range(15)))
check("chosen_linear16_scale_is_bounded_and_not_full_int16_range",
min(values) == -32124 and max(values) == 32124
and all(value % 4 == 0 for value in values)
and -32768 not in values and 32767 not in values)
check("stream_preserves_octet_order_and_one_sample_per_octet",
decode_pcmu(fixture) == expected_fixture
and len(decode_pcmu(fixture)) == len(fixture))
check("stateless_chunk_concatenation_including_empty_boundaries",
decode_pcmu(b"") == () and pack_linear16(b"") == b""
and all(decode_pcmu(fixture[:split]) + decode_pcmu(fixture[split:])
== expected_fixture for split in range(len(fixture) + 1)))
le = pack_linear16(fixture, "little")
be = pack_linear16(fixture, "big")
check("explicit_little_and_big_endian_raw_signed16_known_vectors",
le.hex() == "000000000800f8ff7c7d848284007cff"
and be.hex() == "000000000008fff87d7c82840084ff7c")
all_wire = bytes(range(256))
check("all_packed_samples_match_independent_signed_bit_weight_reference",
all(len(pack_linear16(all_wire, order)) == 512
and all(signed16_weight_reference(
pack_linear16(all_wire, order)[2 * index:2 * index + 2], order)
== reference[index] for index in range(256))
for order in ("little", "big")))
check("teaching_cap_boundary_is_inclusive_and_oversize_rejected",
len(decode_pcmu(b"\xff" * MAX_SAMPLES)) == MAX_SAMPLES
and len(pack_linear16(b"\x80" * MAX_SAMPLES)) == 2 * MAX_SAMPLES
and must_raise(ValueError, decode_pcmu, b"\x00" * (MAX_SAMPLES + 1))
and must_raise(ValueError, pack_linear16, b"\x00" * (MAX_SAMPLES + 1)))
class IntSubclass(int):
pass
class BytesSubclass(bytes):
pass
class StrSubclass(str):
pass
check("scalar_contract_rejects_bool_nonint_subclass_and_out_of_range",
all(must_raise(TypeError, decode_octet, bad)
for bad in (True, False, 1.0, "255", None, b"\xff", IntSubclass(255)))
and all(must_raise(ValueError, decode_octet, bad) for bad in (-1, 256)))
check("stream_contract_rejects_mutable_text_sequence_and_subclass",
all(must_raise(TypeError, function, bad)
for function in (decode_pcmu, pack_linear16)
for bad in (bytearray(b"\xff"), memoryview(b"\xff"), "ff", [255],
(255,), None, BytesSubclass(b"\xff"))))
check("container_byte_order_is_an_explicit_strict_contract",
all(must_raise(TypeError, pack_linear16, fixture, bad)
for bad in (None, True, 1, b"little", StrSubclass("little")))
and all(must_raise(ValueError, pack_linear16, fixture, bad)
for bad in ("native", "Little", "", "middle")))
assert len(checks) == 14
result = {
"python": platform.python_version(),
"dependencies": "standard library only",
"encoding_contract": "ordinary PCMU, no all-zero-character suppression",
"output_units": "integer linear16 container units; 4 times G.711 table2 normalized14 values",
"max_samples": MAX_SAMPLES,
"fixture_hex": fixture.hex(),
"fixture_linear16": list(decode_pcmu(fixture)),
"fixture_pcm16_little_endian_hex": le.hex(),
"fixture_pcm16_big_endian_hex": be.hex(),
"normalized14_endpoints": [-8031, 8031],
"linear16_endpoints": [min(values), max(values)],
"zero_octets_hex": ["7f", "ff"],
"distinct_output_values": len(set(values)),
"within_segment_steps_linear16": list(steps16),
"independent_table_cases": len(reference),
"unique_executed_checks": len(checks),
"checks": checks,
"all_passed": all(checks.values()),
}
print(json.dumps(result, ensure_ascii=True, indent=2))
if __name__ == "__main__":
main()
تمامِ کد، شاملِ آزمونها و مرجعِ مستقل، بالاست. اجرای ثبتشده با پایتونِ ۳٫۱۴٫۵ و فقط کتابخانهٔ استاندارد انجام شده است. برنامه شبکه، فایلِ صوتی یا ابزارِ شنیدن را فراخوانی نمیکند. نتیجهٔ واقعیِ همان اجرا در ادامه آمده؛ عددهای نمونه و نامِ گروهها از خروجیِ ذخیرهشده برداشته شدهاند.
{
"python": "3.14.5",
"dependencies": "standard library only",
"encoding_contract": "ordinary PCMU, no all-zero-character suppression",
"output_units": "integer linear16 container units; 4 times G.711 table2 normalized14 values",
"max_samples": 4096,
"fixture_hex": "ff7ffe7e8000ef6f",
"fixture_linear16": [
0,
0,
8,
-8,
32124,
-32124,
132,
-132
],
"fixture_pcm16_little_endian_hex": "000000000800f8ff7c7d848284007cff",
"fixture_pcm16_big_endian_hex": "000000000008fff87d7c82840084ff7c",
"normalized14_endpoints": [
-8031,
8031
],
"linear16_endpoints": [
-32124,
32124
],
"zero_octets_hex": [
"7f",
"ff"
],
"distinct_output_values": 255,
"within_segment_steps_linear16": [
8,
16,
32,
64,
128,
256,
512,
1024
],
"independent_table_cases": 256,
"unique_executed_checks": 14,
"checks": {
"directed_table_zero_low_step_and_extreme_vectors": true,
"all_256_octets_match_independent_normalized14_segment_table": true,
"two_wire_zeros_and_exactly_255_distinct_reconstruction_values": true,
"flipping_wire_sign_bit_negates_every_reconstruction": true,
"positive_levels_increase_with_declared_within_segment_spacing": true,
"chosen_linear16_scale_is_bounded_and_not_full_int16_range": true,
"stream_preserves_octet_order_and_one_sample_per_octet": true,
"stateless_chunk_concatenation_including_empty_boundaries": true,
"explicit_little_and_big_endian_raw_signed16_known_vectors": true,
"all_packed_samples_match_independent_signed_bit_weight_reference": true,
"teaching_cap_boundary_is_inclusive_and_oversize_rejected": true,
"scalar_contract_rejects_bool_nonint_subclass_and_out_of_range": true,
"stream_contract_rejects_mutable_text_sequence_and_subclass": true,
"container_byte_order_is_an_explicit_strict_contract": true
},
"all_passed": true
}
برای دنبالهٔ ff7ffe7e8000ef6f، خروجی به ترتیب صفر، صفر، هشت، منفیِ هشت، ۳۲۱۲۴، منفیِ ۳۲۱۲۴، ۱۳۲ و منفیِ ۱۳۲ است. هشت بایتِ ورودی، هشت عدد و شانزده بایتِ ظرفِ خام میدهد. این افزایشِ اندازه، بازسازیِ نمایش است و افزایشِ اطلاعاتِ ثبتشدهٔ صوت را ثابت نمیکند.
۴. هر ۲۵۶ بایت را با جدولِ مستقل بسنجیم

اگر مرجعِ آزمون همان فرمولِ تابع را دوباره بنویسد، خطای مشترک میتواند پنهان بماند. مرجعِ این برنامه از مسیرِ دیگری میرود: پایه و فاصلهٔ سطحهای هر قطعه را در واحدِ چهاردهبیتی نگه میدارد، فهرستِ سطحها را میسازد و کدهای هر نیمهٔ علامت را جدا به آن وصل میکند. در آن، تابعِ اصلی یا جداسازیِ بیتهای فرمول فراخوانی نمیشود.
برابریِ همهٔ ۲۵۶ نتیجه با این جدول، یک گروهِ آزمون است. وارونگیِ علامت، افزایشِ سطحهای مثبت و فاصلهٔ داخلِ قطعهها ویژگیهای دیگری هستند که جدا کنترل میشوند. گامهای داخلِ قطعه در مقیاسِ خروجی از هشت تا ۱۰۲۴ تغییر میکنند؛ از این ویژگی، یکنواختیِ فاصلهٔ بینِ همهٔ نمونههای ممکن نتیجه نمیگیریم.
بستهبندی نیز فقط با خواندنِ دوباره توسط همان ابزار تأیید نمیشود. مرجعِ دوم، وزنِ بیتهای ظرفِ علامتدار را مستقیم جمع میکند و هر دو ترتیبِ بایت را میسنجد. این کار خطاهایی مانند جابهجاییِ دو بایت یا از دست رفتنِ علامت را از حسابِ سطحِ PCMU جدا میکند.
در مجموع چهارده گروهِ نامدار اجرا شدهاند. گروهِ جدولِ کامل، ۲۵۶ مورد دارد؛ این تعداد را چهارده به علاوهٔ ۲۵۶ نمیشماریم. ورودیِ خالی، مرزِ سقف، شکستنِ جریان به قطعهها و ردِ نوعهای نامعتبر نیز بررسی شدهاند. خروجیِ خطا در اجرای نهایی خالی است و رسید، نسخه و چکیدهٔ بایتهای کد و خروجی را ثبت میکند.
حتی این پوششِ کاملِ بایتها، برچسبِ قالبِ بیرونی را تأیید نمیکند. اگر بایتِ PCMA را به تابعِ PCMU بدهیم، برنامه عددی میسازد ولی معنای درستِ منبع تضمین نمیشود. همچنین دربارهٔ ترتیبِ زمانی، خرابیِ انتقال، تعدادِ کانال یا کالیبراسیونِ دستگاه چیزی از صرفِ عبورِ این آزمونها نمیفهمیم.
۵. چهار پرسش دربارهٔ صفر، مقیاس و کیفیت

پرسشِ اول: آیا پرکردنِ ورودی با بایتِ صفر، نمونههای صفر میسازد؟ خیر. در نگاشتِ عادیِ این درس، 00 به منفیِ ۳۲۱۲۴ میرسد. دو نمایشِ صفر FF و 7F هستند. برای ساختنِ دادهٔ آزمایشی باید کدِ مناسبِ همان قالب را انتخاب کرد؛ معنای صفرِ حافظه با صفرِ دامنه یکی نیست.
پرسشِ دوم: چرا بزرگترین مقدارِ مثبت ۳۲۷۶۷ نیست؟ چون ظرفِ ذخیرهسازی و مجموعهٔ سطحهای بازسازی دو چیزند. بیشینهٔ عادیِ جدول ۸۰۳۱ است و قراردادِ ضربدرچهار، ۳۲۱۲۴ میدهد. کشیدنِ خودکارِ این مقدار تا انتهای ظرف، بهرهٔ تازهای به داده اعمال میکند و دیگر همان مقیاسِ ثابتِ اعلامشده نخواهد بود.
پرسشِ سوم: آیا از صفرِ بازشده میتوان بایتِ اصلی را شناخت؟ بهتنهایی خیر؛ هر دو کدِ صفر به همان عدد میرسند. نتیجهٔ پیمایش، ۲۵۵ مقدارِ متمایز برای ۲۵۶ ورودی است. اگر حفظِ هویتِ بایتِ اصلی لازم باشد، باید خودِ جریانِ کدشده یا اطلاعاتِ جدا نگهداری شود؛ اضافهکردنِ یک رمزگذار این ابهام را خودبهخود برطرف نمیکند.
پرسشِ چهارم: آیا عبورِ همهٔ آزمونها کیفیتِ تماس را ثابت میکند؟ خیر. آزمونها فقط قراردادِ دقیقِ بازسازی و ظرفِ خام را در دامنهٔ محدود میسنجند. کیفیتِ شنیداری به سیگنال، مراحلِ دیگر و روشِ ارزیابی وابسته است. حتی برای شنیدنِ صحیح، مصرفکننده باید نرخ، کانالها، قالب و ترتیبِ بایتِ خروجی را جدا بداند؛ این تابع آن فراداده را حدس نمیزند.
منابع و مطالعهٔ بیشتر
- ITU-T G.711؛ بخش۳٫۶ و جدولهای۲الف/۲ب، بازسازیِ عادیِ قانونِ مو
- RFC3551؛ بخش۴٫۵٫۱۴، قراردادِ PCMA و PCMU
- ITU-T STL v2024؛ سرآیندِ ماژول و تابعِ کاملِ ulaw_expand، نسخهٔ ثابت
بررسی منابع: ۲۷ مهر ۱۴۰۵. تحلیلها و پیشنهادهای اجرایی، جمعبندی نویسندهاند.

