mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 19:45:09 +00:00
fix(tci): Stream.length — вещественные отсчёты всего блока, а не на канал
MSHV не декодировал FT4 по TCI (по виртуальному кабелю — декодировал).
Регрессия из dc6f996: там length аудио был переведён на «сэмплы на канал»
по формулировке §4.3, а живые клиенты считают по нему БАЙТЫ блока
(network.cpp: `int cr2 = pStream->length*bit_s;` с шагом `chan*bit_s`,
на передаче `quint32 cr3 = pStream->length*bit_s;`). При Channels=2
(умолчание MSHV) клиент разбирал половину каждого блока: звук с дырами
50%, водопад шире и грязнее, декодер разваливался. В моно дефекта не
видно — единицы совпадают, поэтому первый прогон стенда увёл в сторону.
Правка верна и по документу, а не только по клиенту: §3.4 после IQ
(«количество вещественных отсчётов… комплексных = length/channels»)
говорит «аудиопоток приёмника ПОЛНОСТЬЮ ПОВТОРЯЕТ IQ поток» и
перечисляет ровно три отличия — каналы, формат сэмплов, число сэмплов в
пакете. Единиц length среди них нет, поле в struct Stream одно.
Развилка по типу потока была вычитана из воздуха.
§4.3 путает две величины, и её формулировка верна лишь для моно:
AUDIO_STREAM_SAMPLES — кадры НА КАНАЛ, Stream.length — отсчёты ВСЕГО
блока. Что arg1 считает кадры, видно из самой §4.3 дважды: минимум
512/256/128/100 на 48/24/12/8 кГц даёт обещанные «не меньше 10 мс»
только при счёте на канал, и потолок data[16384] = 2048 × 2 × float32.
TX_CHRONO замыкает круг: клиент шлёт столько отсчётов, сколько названо
в length маркера, и возвращает то же число обратно.
Размер блока не менялся (2048@48к = 42.7 мс). Гипотеза про клиппинг
тапа RX_AUDIO проверена замером и снята: пик 0.115.
Стенд: проверки length переписаны на инвариант «байт = length × размер
отсчёта»; новый test/tci/ft4_bench.py снимает поток TCI и PipeWire-
источник одновременно, режет на нарезки FT4 по общим часам и гоняет
через настоящий jt9 --ft4 (блок разбирает КАК MSHV — этим и поймал).
Проверено вживую: MSHV декодирует FT4 по TCI.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Executable
+377
@@ -0,0 +1,377 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Стенд декодирования FT4: аудио TCI против виртуального кабеля, бок о бок.
|
||||
|
||||
Зачем: MSHV по виртуальному аудиокабелю FT4 декодирует, а по TCI — нет, хотя
|
||||
источник звука один и тот же. Стенд снимает оба потока ОДНОВРЕМЕННО, режет их
|
||||
на нарезки FT4 по единым часам и отдаёт каждую нарезку настоящему декодеру
|
||||
(`jt9 --ft4` из WSJT-X). Дальше остаётся смотреть в таблицу, а не гадать.
|
||||
|
||||
Три колонки не случайны:
|
||||
|
||||
cable — эталон: то, что слышит MSHV сейчас (PipeWire-источник, обычно
|
||||
`wsjtx_tx_source`);
|
||||
tci — наш поток как есть, включая обрезание по ±1.0 при укладке в int16
|
||||
(ровно то, что делает клиент, попросивший int16);
|
||||
tci-n — тот же поток, но нормированный по пику перед укладкой в WAV.
|
||||
|
||||
Если декодируется `tci-n`, а `tci` — нет, значит дело в УРОВНЕ: тап RX_AUDIO
|
||||
снят до регулятора громкости, и пики WDSP выходят за ±1.0 (в этом проекте они
|
||||
доходили до 2.07). Всё, что выше единицы, у клиента срезается — сигнал в
|
||||
водопаде становится шире и грязнее, а декодер разваливается. Если не
|
||||
декодируется ни то, ни другое, а cable декодируется — смотреть надо на
|
||||
непрерывность потока (потерянные блоки) и на частоту дискретизации.
|
||||
|
||||
Стенд ТОЛЬКО СЛУШАЕТ: ни одной команды, способной тронуть передачу.
|
||||
|
||||
Запуск (приложение работает, TCI поднят, в MSHV идёт приём FT4):
|
||||
test/tci/ft4_bench.py --rx 1 --windows 4
|
||||
test/tci/ft4_bench.py --rx 0 --device wsjtx_tx_source --keep
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import math
|
||||
import os
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import wave
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from live_rx_test import WS # noqa: E402
|
||||
|
||||
HDR = struct.Struct("<16I")
|
||||
STREAM_RX_AUDIO = 1
|
||||
FMT_FLOAT32 = 3
|
||||
RATE = 12000 # то, что ждёт jt9
|
||||
FT4_PERIOD = 7.5 # длина нарезки FT4, с
|
||||
FT4_SAMPLES = int(RATE * FT4_PERIOD)
|
||||
|
||||
|
||||
# ── съём звука ───────────────────────────────────────────────────────────
|
||||
class TciCapture(threading.Thread):
|
||||
"""RX-аудио одного приёмника TCI: float32, моно, 12 кГц.
|
||||
|
||||
Просим именно float32 — иначе истинный уровень не увидеть: в int16 сервер
|
||||
сам зажимает всё, что выше ±1.0, и стенд мерил бы уже обрезанное.
|
||||
"""
|
||||
|
||||
def __init__(self, host, port, rx, channels=2, like_mshv=True):
|
||||
super().__init__(daemon=True)
|
||||
self.ws = WS(host, port)
|
||||
self.rx = rx
|
||||
self.channels = channels
|
||||
# ★Разбирать блок КАК MSHV: он берёт число байт из поля length
|
||||
# (`cr2 = pStream->length*bit_s`) и шагает кадрами `chan*bit_s`, то
|
||||
# есть считает length вещественными отсчётами ВСЕГО блока. Если сервер
|
||||
# понимает length как «на канал», у стерео разбирается ровно половина
|
||||
# блока — и звук у клиента идёт с дырами. Режим --raw читает всю
|
||||
# полезную нагрузку, игнорируя length: так видно, что данные-то целы.
|
||||
self.like_mshv = like_mshv
|
||||
self.short_blocks = 0
|
||||
self.samples = [] # float
|
||||
self.t0 = None # время первого отсчёта, monotonic
|
||||
self.blocks = 0
|
||||
self.bad_rate = 0
|
||||
self.stop_flag = False
|
||||
self.lock = threading.Lock()
|
||||
|
||||
def start_stream(self):
|
||||
self.ws.send_text(f"audio_samplerate:{RATE};")
|
||||
self.ws.send_text("audio_stream_sample_type:float32;")
|
||||
self.ws.send_text(f"audio_stream_channels:{self.channels};")
|
||||
self.ws.send_text("audio_stream_samples:512;")
|
||||
self.ws.send_text(f"audio_start:{self.rx};")
|
||||
|
||||
def run(self):
|
||||
while not self.stop_flag:
|
||||
frame = self.ws.recv_frame(0.3)
|
||||
if not frame:
|
||||
continue
|
||||
op, payload = frame
|
||||
if op != 2 or len(payload) < HDR.size:
|
||||
continue
|
||||
head = HDR.unpack(payload[:HDR.size])
|
||||
receiver, srate, fmt, length = head[0], head[1], head[2], head[5]
|
||||
stype, chans = head[6], head[7]
|
||||
if stype != STREAM_RX_AUDIO or receiver != self.rx:
|
||||
continue
|
||||
if srate != RATE:
|
||||
self.bad_rate += 1
|
||||
continue
|
||||
data = payload[HDR.size:]
|
||||
if self.like_mshv:
|
||||
want = int(length) * 4 # cr2 = length * bit_s
|
||||
if want < len(data):
|
||||
self.short_blocks += 1
|
||||
data = data[:min(want, len(data))]
|
||||
n = len(data) // 4
|
||||
vals = struct.unpack(f"<{n}f", data[:n * 4])
|
||||
if chans == 2: # берём левый канал
|
||||
vals = vals[0::2]
|
||||
now = time.monotonic()
|
||||
with self.lock:
|
||||
if self.t0 is None:
|
||||
self.t0 = now - len(vals) / RATE
|
||||
self.samples.extend(vals)
|
||||
self.blocks += 1
|
||||
_ = length
|
||||
|
||||
def close(self):
|
||||
self.stop_flag = True
|
||||
try:
|
||||
self.ws.send_text(f"audio_stop:{self.rx};")
|
||||
time.sleep(0.2)
|
||||
except OSError:
|
||||
pass
|
||||
self.ws.close()
|
||||
|
||||
|
||||
class CableCapture(threading.Thread):
|
||||
"""Виртуальный кабель через parec: моно, 12 кГц, s16 — эталонная дорожка."""
|
||||
|
||||
def __init__(self, device):
|
||||
super().__init__(daemon=True)
|
||||
self.proc = subprocess.Popen(
|
||||
["parec", f"--device={device}", "--format=s16le",
|
||||
f"--rate={RATE}", "--channels=1", "--latency-msec=50"],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
|
||||
self.samples = []
|
||||
self.t0 = None
|
||||
self.stop_flag = False
|
||||
self.lock = threading.Lock()
|
||||
|
||||
def run(self):
|
||||
while not self.stop_flag:
|
||||
block = self.proc.stdout.read(4096)
|
||||
if not block:
|
||||
break
|
||||
n = len(block) // 2
|
||||
vals = struct.unpack(f"<{n}h", block[:n * 2])
|
||||
now = time.monotonic()
|
||||
with self.lock:
|
||||
if self.t0 is None:
|
||||
self.t0 = now - n / RATE
|
||||
self.samples.extend(v / 32768.0 for v in vals)
|
||||
|
||||
def close(self):
|
||||
self.stop_flag = True
|
||||
try:
|
||||
self.proc.terminate()
|
||||
self.proc.wait(timeout=2)
|
||||
except Exception:
|
||||
self.proc.kill()
|
||||
|
||||
|
||||
# ── нарезка, WAV, декодер ────────────────────────────────────────────────
|
||||
def cut(cap, t_start, count):
|
||||
"""Отсчёты, начинающиеся в момент t_start (monotonic). None — не хватило."""
|
||||
with cap.lock:
|
||||
if cap.t0 is None:
|
||||
return None
|
||||
first = int(round((t_start - cap.t0) * RATE))
|
||||
if first < 0 or first + count > len(cap.samples):
|
||||
return None
|
||||
return list(cap.samples[first:first + count])
|
||||
|
||||
|
||||
def stats(vals):
|
||||
if not vals:
|
||||
return 0.0, 0.0, 0.0
|
||||
peak = max(abs(v) for v in vals)
|
||||
rms = math.sqrt(sum(v * v for v in vals) / len(vals))
|
||||
over = sum(1 for v in vals if abs(v) >= 1.0)
|
||||
return peak, rms, 100.0 * over / len(vals)
|
||||
|
||||
|
||||
def write_wav(path, vals, normalize):
|
||||
scale = 1.0
|
||||
if normalize:
|
||||
peak = max((abs(v) for v in vals), default=0.0)
|
||||
if peak > 1e-9:
|
||||
scale = 0.9 / peak
|
||||
frames = bytearray()
|
||||
for v in vals:
|
||||
s = int(round(v * scale * 32767.0))
|
||||
frames += struct.pack("<h", max(-32768, min(32767, s)))
|
||||
with wave.open(path, "wb") as w:
|
||||
w.setnchannels(1)
|
||||
w.setsampwidth(2)
|
||||
w.setframerate(RATE)
|
||||
w.writeframes(bytes(frames))
|
||||
|
||||
|
||||
def decode(path, workdir):
|
||||
"""jt9 --ft4. Возвращает список строк декодера."""
|
||||
try:
|
||||
res = subprocess.run(
|
||||
["jt9", "--ft4", "-p", "7.5", "-d", "3", "-L", "100", "-H", "3000",
|
||||
"-a", workdir, "-t", workdir, os.path.abspath(path)],
|
||||
cwd=workdir, capture_output=True, text=True, timeout=60)
|
||||
except FileNotFoundError:
|
||||
print("jt9 не найден — поставьте WSJT-X", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
except subprocess.TimeoutExpired:
|
||||
return ["<таймаут декодера>"]
|
||||
out = []
|
||||
for line in res.stdout.splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("<Decode"):
|
||||
continue
|
||||
out.append(line)
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(
|
||||
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("--host", default="127.0.0.1")
|
||||
ap.add_argument("--port", type=int, default=40001)
|
||||
ap.add_argument("--rx", type=int, default=1,
|
||||
help="приёмник TCI (1 = «TCI Client rx2» в MSHV)")
|
||||
ap.add_argument("--device", default="wsjtx_tx_source",
|
||||
help="источник PipeWire с тем же звуком (виртуальный кабель)")
|
||||
ap.add_argument("--windows", type=int, default=4,
|
||||
help="сколько нарезок FT4 снять (по 7.5 с)")
|
||||
ap.add_argument("--channels", type=int, default=2, choices=(1, 2),
|
||||
help="каналов в потоке TCI (2 = как просит MSHV)")
|
||||
ap.add_argument("--raw", action="store_true",
|
||||
help="читать всю полезную нагрузку, игнорируя поле length "
|
||||
"(MSHV так НЕ умеет — см. шапку)")
|
||||
ap.add_argument("--no-cable", action="store_true",
|
||||
help="без эталона: только поток TCI")
|
||||
ap.add_argument("--keep", action="store_true",
|
||||
help="оставить WAV-файлы и назвать каталог")
|
||||
args = ap.parse_args()
|
||||
|
||||
workdir = tempfile.mkdtemp(prefix="ft4bench-")
|
||||
print(f"Стенд FT4: приёмник TCI {args.rx}"
|
||||
+ ("" if args.no_cable else f", эталон — {args.device}"))
|
||||
print(f"рабочий каталог: {workdir}")
|
||||
|
||||
tci = TciCapture(args.host, args.port, args.rx, args.channels,
|
||||
like_mshv=not args.raw)
|
||||
cable = None
|
||||
try:
|
||||
tci.start()
|
||||
time.sleep(0.5)
|
||||
tci.start_stream()
|
||||
if not args.no_cable:
|
||||
cable = CableCapture(args.device)
|
||||
cable.start()
|
||||
|
||||
# Ждём, пока пойдут оба потока: иначе первая нарезка окажется пустой.
|
||||
deadline = time.monotonic() + 5
|
||||
while time.monotonic() < deadline:
|
||||
if tci.t0 is not None and (cable is None or cable.t0 is not None):
|
||||
break
|
||||
time.sleep(0.1)
|
||||
if tci.t0 is None:
|
||||
print(" FAIL блоков RX-аудио от TCI нет вовсе"
|
||||
+ (f" (заголовков с чужой частотой: {tci.bad_rate})"
|
||||
if tci.bad_rate else ""))
|
||||
return 1
|
||||
if cable is not None and cable.t0 is None:
|
||||
print(f" FAIL с источника {args.device} звук не идёт")
|
||||
return 1
|
||||
|
||||
print(f"{'нарезка':>8} | {'cable':>5} | {'tci':>5} | {'tci-n':>5} | "
|
||||
f"уровень TCI (пик / RMS / за ±1.0)")
|
||||
print("-" * 78)
|
||||
|
||||
# Нарезки FT4 идут по единым часам: старт кратен 7.5 с от начала минуты.
|
||||
wall = time.time()
|
||||
mono = time.monotonic()
|
||||
n_ok = {"cable": 0, "tci": 0, "tcin": 0}
|
||||
done = 0
|
||||
# ★Потолок по времени: если поток идёт медленнее реального времени
|
||||
# (клиент разбирает не весь блок), нарезка не наберётся НИКОГДА, и без
|
||||
# этого потолка стенд просто висел бы. Само по себе срабатывание —
|
||||
# диагноз, поэтому ниже печатается темп набора.
|
||||
started = time.monotonic()
|
||||
limit = started + args.windows * FT4_PERIOD * 3 + 25
|
||||
while done < args.windows:
|
||||
if time.monotonic() > limit:
|
||||
with tci.lock:
|
||||
got = len(tci.samples) / RATE
|
||||
elapsed = time.monotonic() - started
|
||||
print("-" * 78)
|
||||
print(f" FAIL нарезки не набираются: за {elapsed:.1f} с "
|
||||
f"реального времени поток TCI дал {got:.1f} с звука "
|
||||
f"({100.0 * got / max(elapsed, 0.1):.0f}% темпа)")
|
||||
if tci.short_blocks:
|
||||
print(f" ★и {tci.short_blocks} блоков разобраны НЕ ЦЕЛИКОМ: "
|
||||
f"поле length меньше присланных данных — клиент вроде "
|
||||
f"MSHV теряет остаток каждого блока")
|
||||
break
|
||||
now_wall = time.time()
|
||||
seq = math.floor(now_wall / FT4_PERIOD) * FT4_PERIOD
|
||||
t_start_wall = seq - FT4_PERIOD # предыдущая, уже целая
|
||||
t_start = mono + (t_start_wall - wall)
|
||||
# Ждём, пока накопится хвост нарезки у обоих потоков.
|
||||
block_tci = cut(tci, t_start, FT4_SAMPLES)
|
||||
block_cab = None if cable is None else cut(cable, t_start, FT4_SAMPLES)
|
||||
if block_tci is None or (cable is not None and block_cab is None):
|
||||
time.sleep(0.3)
|
||||
continue
|
||||
|
||||
label = time.strftime("%H:%M:%S", time.localtime(t_start_wall))
|
||||
peak, rms, over = stats(block_tci)
|
||||
|
||||
f_tci = os.path.join(workdir, f"tci_{label.replace(':', '')}.wav")
|
||||
f_tcn = os.path.join(workdir, f"tcin_{label.replace(':', '')}.wav")
|
||||
write_wav(f_tci, block_tci, normalize=False)
|
||||
write_wav(f_tcn, block_tci, normalize=True)
|
||||
d_tci = decode(f_tci, workdir)
|
||||
d_tcn = decode(f_tcn, workdir)
|
||||
d_cab = []
|
||||
if block_cab is not None:
|
||||
f_cab = os.path.join(workdir, f"cable_{label.replace(':', '')}.wav")
|
||||
write_wav(f_cab, block_cab, normalize=False)
|
||||
d_cab = decode(f_cab, workdir)
|
||||
|
||||
n_ok["cable"] += len(d_cab)
|
||||
n_ok["tci"] += len(d_tci)
|
||||
n_ok["tcin"] += len(d_tcn)
|
||||
print(f"{label:>8} | {len(d_cab):>5} | {len(d_tci):>5} | "
|
||||
f"{len(d_tcn):>5} | пик {peak:.3f} RMS {rms:.3f} "
|
||||
f"{over:.2f}%")
|
||||
for src, lines in (("cable", d_cab), ("tci", d_tci), ("tci-n", d_tcn)):
|
||||
for line in lines[:3]:
|
||||
print(f" {src:>5}: {line}")
|
||||
done += 1
|
||||
# Следующая нарезка появится не раньше чем через период.
|
||||
time.sleep(FT4_PERIOD * 0.9)
|
||||
|
||||
print("-" * 78)
|
||||
print(f"Итого декодов: cable {n_ok['cable']}, tci {n_ok['tci']}, "
|
||||
f"tci-n {n_ok['tcin']}")
|
||||
if tci.short_blocks:
|
||||
print(f"★блоков, у которых length меньше присланных данных: "
|
||||
f"{tci.short_blocks} — клиент вроде MSHV разбирает только "
|
||||
f"часть блока, остальное теряется")
|
||||
if n_ok["tci"] == 0 and n_ok["tcin"] > 0:
|
||||
print("⇒ дело в УРОВНЕ: поток TCI режется по ±1.0 (см. шапку стенда)")
|
||||
elif n_ok["tci"] == 0 and n_ok["cable"] > 0:
|
||||
print("⇒ уровень ни при чём: смотреть непрерывность потока и частоту")
|
||||
finally:
|
||||
tci.close()
|
||||
if cable is not None:
|
||||
cable.close()
|
||||
if not args.keep:
|
||||
for name in os.listdir(workdir):
|
||||
try:
|
||||
os.unlink(os.path.join(workdir, name))
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
os.rmdir(workdir)
|
||||
except OSError:
|
||||
pass
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
+19
-13
@@ -100,9 +100,10 @@ begin
|
||||
Check('header receiver', H.Receiver = 2);
|
||||
Check('header rate', H.SampleRate = 12000);
|
||||
Check('header format', H.Format = LongWord(Ord(tsyInt16)));
|
||||
// ★Аудио: length — сэмплы НА КАНАЛ, ровно то число, что назвал
|
||||
// AUDIO_STREAM_SAMPLES (§4.3). Раньше сюда уходило вдвое больше.
|
||||
Check('header length аудио = на канал', H.DataLength = 512);
|
||||
// ★length — вещественные отсчёты ВСЕГО блока, и у аудио тоже: клиенты
|
||||
// считают по нему число байт (MSHV: `cr2 = length*bit_s`). Объявишь «на
|
||||
// канал» — у стерео разберётся половина блока, и звук пойдёт с дырами.
|
||||
Check('header length аудио = ×каналы', H.DataLength = 1024);
|
||||
Check('header type', H.StreamType = LongWord(Ord(tstRXAudio)));
|
||||
Check('header channels', H.Channels = 2);
|
||||
Check('header codec/crc', (H.Codec = 0) and (H.CRC = 0));
|
||||
@@ -111,10 +112,13 @@ begin
|
||||
// комплексных (§3.4 прямо: комплексных = length/channels).
|
||||
TCIFillHeader(H, tstIQ, 0, 48000, tsyFloat32, 512, 2);
|
||||
Check('header length IQ = ×каналы', H.DataLength = 1024);
|
||||
// Маркер времени TX_CHRONO живёт по правилам аудио: он называет клиенту
|
||||
// Маркер времени TX_CHRONO живёт по тем же правилам: он называет клиенту
|
||||
// размер блока, который тот пришлёт (§3.4).
|
||||
TCIFillHeader(H, tstTXChrono, 0, 12000, tsyFloat32, 256, 2);
|
||||
Check('header length chrono = на канал', H.DataLength = 256);
|
||||
Check('header length chrono = ×каналы', H.DataLength = 512);
|
||||
// Моно: отсчётов столько же, сколько сэмплов на канал.
|
||||
TCIFillHeader(H, tstRXAudio, 0, 12000, tsyInt16, 512, 1);
|
||||
Check('header length моно = сэмплы', H.DataLength = 512);
|
||||
|
||||
// Упаковка/распаковка: сквозной проход по всем форматам.
|
||||
for i := 0 to 7 do Src[i] := (i - 4) / 8.0; // −0.5 … +0.375
|
||||
@@ -426,10 +430,12 @@ begin
|
||||
if Length(Pay) < SizeOf(H) then begin OkHdr := False; Continue; end;
|
||||
Move(Pay[0], H, SizeOf(H));
|
||||
if (H.SampleRate <> 12000) or (H.Channels <> 2) or
|
||||
(H.DataLength <> 512) or // сэмплов НА КАНАЛ
|
||||
(H.DataLength <> 1024) or // вещественных отсчётов = 512 × 2
|
||||
(H.StreamType <> LongWord(Ord(tstRXAudio))) or
|
||||
(H.Format <> LongWord(Ord(tsyFloat32))) or
|
||||
(Length(Pay) <> SizeOf(H) + 512 * 2 * 4) then OkHdr := False;
|
||||
// ★Правило, по которому живут клиенты: байт данных = length × размер
|
||||
// отсчёта. Ровно так считает MSHV (`cr2 = length*bit_s`).
|
||||
(Length(Pay) <> SizeOf(H) + Integer(H.DataLength) * 4) then OkHdr := False;
|
||||
Inc(Blocks);
|
||||
end;
|
||||
Check('аудио: 4 блока', Blocks = 4, IntToStr(Blocks));
|
||||
@@ -1279,12 +1285,11 @@ begin
|
||||
if H.StreamType = LongWord(Ord(tstRXAudio)) then
|
||||
begin
|
||||
Inc(AudioBlocks);
|
||||
// В аудиопотоке length — сэмплы НА КАНАЛ (AUDIO_STREAM_SAMPLES), то
|
||||
// есть ровно то, что просили, а байт в блоке length × channels × 4.
|
||||
// length — вещественные отсчёты всего блока: просили 256 сэмплов на
|
||||
// канал, каналов 2, значит 512 отсчётов и столько же × 4 байта.
|
||||
if (H.SampleRate <> 12000) or (H.Receiver <> 0) or
|
||||
(H.DataLength <> 256) or
|
||||
(Length(Pay) <> SizeOf(H) +
|
||||
Integer(H.DataLength) * Integer(H.Channels) * 4) then
|
||||
(H.DataLength <> 256 * Integer(H.Channels)) or
|
||||
(Length(Pay) <> SizeOf(H) + Integer(H.DataLength) * 4) then
|
||||
Inc(BadHdr);
|
||||
end
|
||||
else if H.StreamType = LongWord(Ord(tstIQ)) then
|
||||
@@ -1394,7 +1399,8 @@ begin
|
||||
if H.StreamType = LongWord(Ord(tstTXChrono)) then
|
||||
begin
|
||||
Inc(n);
|
||||
if (H.SampleRate <> 12000) or (H.DataLength <> 256) then
|
||||
if (H.SampleRate <> 12000) or
|
||||
(H.DataLength <> 256 * Integer(H.Channels)) then
|
||||
Inc(BadHdr);
|
||||
end;
|
||||
end;
|
||||
|
||||
Reference in New Issue
Block a user