From ee5c258b3c60cdb7f151c0eabf4cf0d2e2445b92 Mon Sep 17 00:00:00 2001 From: evgeny Date: Fri, 30 Oct 2026 19:28:21 +0100 Subject: [PATCH] Preserve adaptive audio batch target without partial-packet deadlines --- src/call/call_audio_batch.c | 49 +++++++++++++++++++++++++++++++++++++ src/call/call_audio_batch.h | 16 ++++++++++-- 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/src/call/call_audio_batch.c b/src/call/call_audio_batch.c index 25436f717..6454f6342 100644 --- a/src/call/call_audio_batch.c +++ b/src/call/call_audio_batch.c @@ -1,4 +1,5 @@ #include "call_audio_batch.h" +#include "../routing_layer/etcp_router.h" #include "../../lib/debug_config.h" #include @@ -49,3 +50,51 @@ int call_audio_batch_parse(const uint8_t *wire, size_t length, struct call_audio invalid: DEBUG_WARN(DEBUG_CATEGORY_CALL, "audio batch RX malformed bytes=%zu", length); return -1; } + +void call_audio_batch_init(struct call_audio_batch_control *c, uint64_t now) { + if (!c) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio batch reset without state"); return; } + *c = (struct call_audio_batch_control){.target = 4, .last_us = now}; +} +int call_audio_batch_step(struct call_audio_batch_control *c, uint64_t now, const struct ETCP_ROUTER_TX_STATS *net) { + if (!c || !c->target || now < c->last_us || (net && net->last_ack_us > now)) { + DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio batch invalid control sample time=%llu", (unsigned long long)now); return -1; + } + unsigned previous = c->target; + if ((!net && (c->local_epoch || c->peer_epoch || c->feedback_received)) + || (net && (net->local_epoch != c->local_epoch || net->peer_epoch != c->peer_epoch))) { + call_audio_batch_init(c, now); + if (net) { c->local_epoch = net->local_epoch; c->peer_epoch = net->peer_epoch; } + DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "audio batch reset router epoch=%llx/%llx previous=%u", + (unsigned long long)c->local_epoch, (unsigned long long)c->peer_epoch, previous); + } + c->last_us = now; + c->delay_us = net ? net->delivery_rtt_us : 0; + if (net && net->queued_age_us > c->delay_us) c->delay_us = net->queued_age_us; + if (net && net->inflight_age_us > c->delay_us) c->delay_us = net->inflight_age_us; + uint64_t interval = net && net->delivery_rtt_us > 250000 ? net->delivery_rtt_us * 2 : 500000; + uint64_t fresh_us = interval > 2000000 ? interval : 2000000; + int measured = net && net->ready && !net->no_route && !net->receive_busy && !net->probing + && net->acked_packets && now - net->last_ack_us <= fresh_us; + if (measured) c->feedback_received = 1; + int usable = measured && net->window.valid; + const char *reason = "hold"; + if (!usable) { c->high = c->low = 0; reason = "unavailable"; } + else if (c->delay_us > 100000) { + c->low = 0; + if (!c->high) { c->high = 1; c->high_since_us = now; } + if (c->target < CALL_AUDIO_BATCH_MAX && now >= c->next_change_us && now - c->high_since_us >= 200000) { + c->target++; c->next_change_us = now + interval; reason = "enqueue_ack_delay"; + } + } else if (c->delay_us < 40000 && !net->queued_packets) { + c->high = 0; + if (!c->low) { c->low = 1; c->low_since_us = now; } + if (c->target > 1 && now >= c->next_change_us && now - c->low_since_us >= 2000000) { + c->target--; c->next_change_us = now + interval; c->low_since_us = now; reason = "recovered"; + } + } else c->high = c->low = 0; + if (c->target != previous) DEBUG_INFO(DEBUG_CATEGORY_CALL, + "audio batch target=%u->%u reason=%s delay_us=%llu router_rtt_us=%llu queued_us=%llu flight_us=%llu", + previous, c->target, reason, (unsigned long long)c->delay_us, (unsigned long long)(net ? net->delivery_rtt_us : 0), + (unsigned long long)(net ? net->queued_age_us : 0), (unsigned long long)(net ? net->inflight_age_us : 0)); + return 0; +} diff --git a/src/call/call_audio_batch.h b/src/call/call_audio_batch.h index a7a8117ac..72e739ef6 100644 --- a/src/call/call_audio_batch.h +++ b/src/call/call_audio_batch.h @@ -1,5 +1,5 @@ -/* Упаковка Opus звонка: 1..10 кадров, не более 1024 байт. Wire views заимствуют входной буфер. - * call.c собирает доступные кадры по готовности Router, без отдельного ожидания наполнения. */ +/* Упаковка Opus звонка и выбор размера пачки по Router enqueue->ACK. Views заимствуют входной буфер. + * Владелец снимает статистику каждые 50мс. Отправка ждёт target кадров И готовности Router; deadline нет. */ #ifndef CALL_AUDIO_BATCH_H #define CALL_AUDIO_BATCH_H #include "call_proto.h" @@ -15,10 +15,22 @@ struct call_audio_batch_view { struct call_pkt_media header; /* Общие поля; count определяет число frames. */ struct call_audio_frame_view frames[CALL_AUDIO_BATCH_MAX]; }; +struct ETCP_ROUTER_TX_STATS; +struct call_audio_batch_control { + uint64_t local_epoch, peer_epoch, last_us, high_since_us, low_since_us, next_change_us; + uint64_t delay_us; /* max полного RTT и возраста Router-очередей; локальное накопление не входит. */ + unsigned target; /* Необходимое число кадров, 1..10; старт с 4. */ + int feedback_received; /* Первый пригодный ACK в текущей Router epoch. */ + int high, low; /* Гистерезис роста/снижения target. */ +}; /* Encode/parse: 0/-1. Проверяют весь пакет до изменения out/length/view; ошибки логируются. * Encode не допускает перекрытие output с Opus; capacity >= вычисленного размера, максимум 1024. */ int call_audio_batch_encode(const struct call_audio_batch_view *batch, uint8_t *out, size_t capacity, size_t *length); int call_audio_batch_parse(const uint8_t *wire, size_t length, struct call_audio_batch_view *out); +/* Начальное target=4; новый звонок или смена Router epoch сбрасывает историю. */ +void call_audio_batch_init(struct call_audio_batch_control *control, uint64_t now_us); +/* NULL — канал отсутствует; без свежих ACK/при блокировке target не растёт. 0/-1 с логом. */ +int call_audio_batch_step(struct call_audio_batch_control *control, uint64_t now_us, const struct ETCP_ROUTER_TX_STATS *net); #ifdef __cplusplus } #endif