mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 18:43:51 +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:
+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