"""DMRhub v2 · сборка DMR-голоса ИЗ ВОКОДЕРА в эфир (без parrot-шаблонов). Нужно для инжекта чужого голоса (P25→DMR, TTS, приложение) DMR-пирам: собираем голосовой суперкадр целиком в коде, с корректным FEC, без утечки чужой идентичности: • Voice LC Header / Terminator = BPTC(196,96) над [LC9 + RS(12,9) 3Б] (+маска типа); • Voice burst A = AMBE + BS voice sync; • Voice bursts B,C,D,E = AMBE + EMB + фрагменты Embedded LC того же вызова; • Voice burst F = AMBE + EMB (null embedded). Порт проверенного энкодера RadioStar (бит-в-бит против реального захвата): RS(12,9), Embedded LC vBPTC(128,72) с Hamming(16,11)+column parity, EMB QR(16,7,6). BPTC(196,96) берём из v2 `bptc.encode`. Slot type/sync — BS-каркас; на RF MMDVMHost перегенерит под свой Color Code. Чистый stdlib + bptc. Самотест: `python dmr_voice.py`. """ from __future__ import annotations from . import bptc # ── RS(12,9) над GF(256), prim.poly 0x11D, корни a^1..a^3 (сверено с захватом) ── _EXP = [0] * 512 _LOG = [0] * 256 _a = 1 for _i in range(255): _EXP[_i] = _a _LOG[_a] = _i _a <<= 1 if _a & 0x100: _a ^= 0x11D for _i in range(255, 512): _EXP[_i] = _EXP[_i - 255] def _gmul(x: int, y: int) -> int: return 0 if (x == 0 or y == 0) else _EXP[_LOG[x] + _LOG[y]] def _rs_gen(): g = [1] for i in range(3): root = _EXP[(1 + i) % 255] ng = [0] * (len(g) + 1) for j in range(len(g)): ng[j] ^= g[j] ng[j + 1] ^= _gmul(g[j], root) g = ng return g _RSG = _rs_gen() def rs129(lc9: bytes) -> bytes: """9 байт LC → 3 байта RS(12,9) чётности (raw, без маски типа).""" out = list(lc9) + [0, 0, 0] for i in range(9): c = out[i] if c: for j in range(1, 4): out[i + j] ^= _gmul(_RSG[j], c) return bytes(out[9:]) # маски «полного LC» по типу данных (MMDVMHost CDMRFullLC) _MASK_HEADER = (0x96, 0x96, 0x96) _MASK_TERM = (0x99, 0x99, 0x99) # ── Sync (BS-источник, ETSI TS 102 361-1). На RF MMDVMHost перегенерит ── SYNC_BS_VOICE = 0x755FD7DF75F7 SYNC_BS_DATA = 0xDFF57D75DF5D # slot type (CC1-каркас; MMDVMHost подставит свой CC на RF) _SLOTTYPE_CC1 = {1: 0x11B8C, 2: 0x12A59} def _slottype(cc: int, dt: int) -> int: return _SLOTTYPE_CC1[dt] # ── EMB QR(16,7,6) — таблица g4klx MMDVMHost ── _ENC_1676 = ( 0x0000, 0x0273, 0x04E5, 0x0696, 0x09C9, 0x0BBA, 0x0D2C, 0x0F5F, 0x11E2, 0x1391, 0x1507, 0x1774, 0x182B, 0x1A58, 0x1CCE, 0x1EBD, 0x21B7, 0x23C4, 0x2552, 0x2721, 0x287E, 0x2A0D, 0x2C9B, 0x2EE8, 0x3055, 0x3226, 0x34B0, 0x36C3, 0x399C, 0x3BEF, 0x3D79, 0x3F0A, 0x411E, 0x436D, 0x45FB, 0x4788, 0x48D7, 0x4AA4, 0x4C32, 0x4E41, 0x50FC, 0x528F, 0x5419, 0x566A, 0x5935, 0x5B46, 0x5DD0, 0x5FA3, 0x60A9, 0x62DA, 0x644C, 0x663F, 0x6960, 0x6B13, 0x6D85, 0x6FF6, 0x714B, 0x7338, 0x75AE, 0x77DD, 0x7882, 0x7AF1, 0x7C67, 0x7E14, 0x804F, 0x823C, 0x84AA, 0x86D9, 0x8986, 0x8BF5, 0x8D63, 0x8F10, 0x91AD, 0x93DE, 0x9548, 0x973B, 0x9864, 0x9A17, 0x9C81, 0x9EF2, 0xA1F8, 0xA38B, 0xA51D, 0xA76E, 0xA831, 0xAA42, 0xACD4, 0xAEA7, 0xB01A, 0xB269, 0xB4FF, 0xB68C, 0xB9D3, 0xBBA0, 0xBD36, 0xBF45, 0xC151, 0xC322, 0xC5B4, 0xC7C7, 0xC898, 0xCAEB, 0xCC7D, 0xCE0E, 0xD0B3, 0xD2C0, 0xD456, 0xD625, 0xD97A, 0xDB09, 0xDD9F, 0xDFEC, 0xE0E6, 0xE295, 0xE403, 0xE670, 0xE92F, 0xEB5C, 0xEDCA, 0xEFB9, 0xF104, 0xF377, 0xF5E1, 0xF792, 0xF8CD, 0xFABE, 0xFC28, 0xFE5B, ) # LCSS (EMB): связь фрагментов Embedded LC (ETSI) LCSS_SINGLE = 0b00 LCSS_FIRST = 0b01 LCSS_CONT = 0b11 LCSS_LAST = 0b10 def emb16(color_code: int, lcss: int, pi: int = 0) -> list[int]: """16-бит EMB: CC(4)+PI(1)+LCSS(2) + QR(16,7,6) parity → 16 бит MSB-first.""" value = ((color_code & 0x0F) << 3) | ((pi & 1) << 2) | (lcss & 0x03) code = _ENC_1676[value & 0x7F] return [(code >> i) & 1 for i in range(15, -1, -1)] # ── bit-хелперы ── def _bytes_to_bits(d): o = [] for by in d: for i in range(7, -1, -1): o.append((by >> i) & 1) return o def _bits_to_bytes(bits): out = bytearray() for i in range(0, len(bits), 8): b = 0 for j in range(8): b = (b << 1) | bits[i + j] out.append(b) return bytes(out) def _int_bits(v, n): return [(v >> i) & 1 for i in range(n - 1, -1, -1)] # ── Embedded LC vBPTC(128,72): Hamming(16,11)+column parity → 4 фрагмента B..E ── def _csum5(lc9: bytes): accum = sum(lc9[:9]) % 31 return [(accum >> i) & 1 for i in range(4, -1, -1)] def _enc_16114(d): """Hamming(16,11,4) — 5 чётностей по 11 битам (dmr_utils3/hamming.py).""" return [ d[0] ^ d[1] ^ d[2] ^ d[3] ^ d[5] ^ d[7] ^ d[8], d[1] ^ d[2] ^ d[3] ^ d[4] ^ d[6] ^ d[8] ^ d[9], d[2] ^ d[3] ^ d[4] ^ d[5] ^ d[7] ^ d[9] ^ d[10], d[0] ^ d[1] ^ d[2] ^ d[4] ^ d[6] ^ d[7] ^ d[10], d[0] ^ d[2] ^ d[5] ^ d[6] ^ d[8] ^ d[9] ^ d[10], ] def encode_emblc(lc9: bytes) -> dict: """9-байтный Embedded LC → 4 фрагмента по 32 бита (бёрсты B,C,D,E).""" cs = _csum5(lc9) bits = _bytes_to_bits(lc9) # 72 бита for pos, bit in zip((32, 43, 54, 65, 76), cs): bits.insert(pos, bit) for index in range(0, 112, 16): # Hamming(16,11) по 7 строкам par = _enc_16114(bits[index:index + 11]) for hindex, hbit in zip(range(index + 11, index + 16), par): bits.insert(hindex, hbit) for index in range(16): # строка 7 = XOR строк 0..6 bits.insert(index + 112, bits[index] ^ bits[index + 16] ^ bits[index + 32] ^ bits[index + 48] ^ bits[index + 64] ^ bits[index + 80] ^ bits[index + 96]) def frag(cols): return [bits[c + r * 16] for c in cols for r in range(8)] return {1: frag([0, 1, 2, 3]), 2: frag([4, 5, 6, 7]), 3: frag([8, 9, 10, 11]), 4: frag([12, 13, 14, 15])} # ── Link Control + бёрсты ── def lc9(dst: int, src: int, unit: bool = False) -> bytes: """9-байтный LC голосового вызова: FLCO + FID + svcOpts + dst(3) + src(3).""" flco = 0x03 if unit else 0x00 return bytes([flco, 0x00, 0x00]) + dst.to_bytes(3, "big") + src.to_bytes(3, "big") def _data_burst(info196, sync48: int, slottype20: int) -> bytes: """264-битный data-бёрст: info[0:98]+[166:264], slot type[98:108]+[156:166], sync[108:156].""" bits = [0] * 264 bits[0:98] = info196[0:98] bits[166:264] = info196[98:196] st = _int_bits(slottype20, 20) bits[98:108] = st[0:10] bits[156:166] = st[10:20] bits[108:156] = _int_bits(sync48, 48) return _bits_to_bytes(bits) def _full_lc_burst(dst, src, unit, cc, dt, mask) -> bytes: l = lc9(dst, src, unit) par = bytes(p ^ m for p, m in zip(rs129(l), mask)) info = bptc.encode(l + par) # 196 on-air info bits return _data_burst(info, SYNC_BS_DATA, _slottype(cc, dt)) def voice_lc_header(dst, src, unit=False, cc=1) -> bytes: """Бёрст Voice LC Header (data type 1).""" return _full_lc_burst(dst, src, unit, cc, 1, _MASK_HEADER) def terminator(dst, src, unit=False, cc=1) -> bytes: """Бёрст Terminator with LC (data type 2).""" return _full_lc_burst(dst, src, unit, cc, 2, _MASK_TERM) def _put_ambe(ambe27: bytes) -> bytearray: """27 байт AMBE (3×9) → 33-байтный бёрст: AMBE в [0:108]+[156:264], середина = 0.""" out = bytearray(33) out[0:13] = ambe27[0:13] out[13] = ambe27[13] & 0xF0 out[19] = ambe27[13] & 0x0F out[20:33] = ambe27[14:27] return out def ambe_from_burst(burst33: bytes) -> bytes: """Инверсия _put_ambe: из 33-байтного DMR голос-бёрста достать 27Б AMBE (3×9). Середина ([13] млад.ниббл / [19] стар.ниббл) — sync/EMB, не AMBE.""" b = bytes(burst33[:33]).ljust(33, b"\x00") out = bytearray(27) out[0:13] = b[0:13] out[13] = (b[13] & 0xF0) | (b[19] & 0x0F) out[14:27] = b[20:33] return bytes(out) _LCSS_BY_VSEQ = {1: LCSS_FIRST, 2: LCSS_CONT, 3: LCSS_CONT, 4: LCSS_LAST} def voice_burst(ambe27: bytes, vseq: int, dst, src, unit=False, cc=1) -> bytes: """Голосовой бёрст. vseq: 0=A(voice sync), 1..4=B..E(EMB+Embedded LC), 5=F(EMB null).""" buf = _put_ambe(ambe27) bits = _bytes_to_bits(buf) if vseq % 6 == 0: # burst A — голосовой sync bits[108:156] = _int_bits(SYNC_BS_VOICE, 48) else: v = vseq % 6 if v in (1, 2, 3, 4): # B,C,D,E — EMB + фрагмент Embedded LC frag = encode_emblc(lc9(dst, src, unit))[v] emb = emb16(cc, _LCSS_BY_VSEQ[v]) else: # F — EMB single, пустой фрагмент frag = [0] * 32 emb = emb16(cc, LCSS_SINGLE) bits[108:116] = emb[0:8] bits[116:148] = list(frag) bits[148:156] = emb[8:16] return _bits_to_bytes(bits) # ── Самотест: RS-вектор + round-trip LC-заголовка через bptc.decode ── if __name__ == "__main__": ok = True L = bytes.fromhex("000000002706155cc1") raw = rs129(L) hdr = bytes(p ^ m for p, m in zip(raw, _MASK_HEADER)) trm = bytes(p ^ m for p, m in zip(raw, _MASK_TERM)) for name, got, exp in (("RS raw", raw.hex(), "505575"), ("RS header", hdr.hex(), "c6c3e3"), ("RS term", trm.hex(), "c9ccec")): good = got == exp ok = ok and good print(f"{name:12}= {got} (эталон {exp}) {'OK' if good else 'FAIL'}") # header round-trip: собрать бёрст → bptc.decode → те же LC9+parity DST, SRC = 250, 1400001 h = voice_lc_header(DST, SRC, cc=1) dec = bptc.decode(h[:33] if len(h) >= 33 else h) # 12 байт: LC9 + masked parity l = lc9(DST, SRC) exp12 = l + bytes(p ^ m for p, m in zip(rs129(l), _MASK_HEADER)) good = bytes(dec) == exp12 ok = ok and good print(f"{'HDR bptc rt':12}= {bytes(dec).hex()} (эталон {exp12.hex()}) {'OK' if good else 'FAIL'}") # Embedded LC B-E: 4 фрагмента по 32 бита frags = encode_emblc(lc9(DST, SRC)) good = all(len(frags[i]) == 32 for i in (1, 2, 3, 4)) ok = ok and good print(f"{'EmbLC frags':12}= {[len(frags[i]) for i in (1,2,3,4)]} {'OK' if good else 'FAIL'}") # AMBE round-trip: voice_burst(ambe27) → ambe_from_burst == ambe27 (для A и B-бёрстов) for vseq in (0, 1, 5): a27 = bytes(range(27)) rt = ambe_from_burst(voice_burst(a27, vseq, DST, SRC)) g = rt == a27 ok = ok and g print(f"{'AMBE rt v'+str(vseq):12}= {'OK' if g else 'FAIL ' + rt.hex()}") print("=== ИТОГО:", "ВСЁ OK" if ok else "ЕСТЬ ОШИБКИ", "===")