Define protected call startup reserve and explicit queued renderer readiness
This commit is contained in:
1 parent
dff64e2871
commit
a3a54af0fe
5 files changed
+116
-16
No files matched your search
+17
-1
@@ -533,7 +533,8 @@ int call_audio_feed_pcm(uint64_t call_id, const int16_t* pcm, int count, int64_t
|
||||
return call_audio_feed_impl(call_id, pcm, count, 0, NULL, 0, capture_pts_us);
|
||||
}
|
||||
|
||||
int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples) {
|
||||
static int call_audio_render_impl(uint64_t call_id, int16_t* out, int max_samples, struct call_audio_render* state) {
|
||||
if (state) *state = (struct call_audio_render){CALL_AUDIO_WAITING, 0};
|
||||
if (!out || max_samples <= 0) return 0;
|
||||
|
||||
int active = 0, ending = 0;
|
||||
@@ -558,6 +559,7 @@ int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples) {
|
||||
* здесь (аудио-поток) — begin_end лишь выставляет флаг, чтобы не гонять
|
||||
* генератор тонов с GUI-потоком. */
|
||||
if (ending) {
|
||||
if (state) state->phase = CALL_AUDIO_ENDING;
|
||||
if (!g_ended_started) {
|
||||
call_tones_ended_start(tones);
|
||||
g_ended_started = 1;
|
||||
@@ -569,6 +571,11 @@ int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples) {
|
||||
n = max_samples;
|
||||
} else {
|
||||
n = call_jitter_pull(j, out, max_samples);
|
||||
if (state) {
|
||||
state->speech_samples = n;
|
||||
state->phase = n ? CALL_AUDIO_SPEECH : g_playback_started ? CALL_AUDIO_RECOVERING : CALL_AUDIO_WAITING;
|
||||
if (state->phase == CALL_AUDIO_WAITING) return 0;
|
||||
}
|
||||
int previous_gap = g_gap_samples;
|
||||
if (n > 0) {
|
||||
if (!g_playback_started) events |= 1;
|
||||
@@ -612,6 +619,15 @@ int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples) {
|
||||
return n;
|
||||
}
|
||||
|
||||
int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples) {
|
||||
return call_audio_render_impl(call_id, out, max_samples, NULL);
|
||||
}
|
||||
|
||||
int call_audio_render_pcm(uint64_t call_id, int16_t* out, int max_samples, struct call_audio_render* state) {
|
||||
if (!state) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "call render: missing output state"); return 0; }
|
||||
return call_audio_render_impl(call_id, out, max_samples, state);
|
||||
}
|
||||
|
||||
void call_audio_playback_progress(uint64_t call_id, uint64_t epoch, uint64_t position,
|
||||
int64_t at_us, int64_t now_us, int measured) {
|
||||
pthread_mutex_lock(&g_mtx);
|
||||
|
||||
@@ -80,6 +80,16 @@ int call_audio_feed_pcm_muted(uint64_t call_id, const int16_t* pcm, int count, i
|
||||
* Активный движок всегда выдаёт полный блок; 0 означает отсутствие движка/ошибку аргументов. */
|
||||
int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples);
|
||||
|
||||
/* Очередной адаптер использует render_pcm: до первого речевого PCM возвращается 0,
|
||||
* без продвижения PCM-шкалы. Адаптер готовит свою очередь перед разрешением вывода.
|
||||
* После старта блок полный, с маскирующей тишиной/тонами при паузе; speech_samples
|
||||
* отличает речь от заполнения. pull_pcm выше — непрерывный вывод (Android AudioTrack):
|
||||
* начальная тишина занимает реальные позиции устройства и учитывается в шкале.
|
||||
* Оба API принадлежат одному playback worker; смешивать их в одной сессии нельзя. */
|
||||
enum call_audio_render_phase { CALL_AUDIO_WAITING, CALL_AUDIO_SPEECH, CALL_AUDIO_RECOVERING, CALL_AUDIO_ENDING };
|
||||
struct call_audio_render { enum call_audio_render_phase phase; int speech_samples; };
|
||||
int call_audio_render_pcm(uint64_t call_id, int16_t* out, int max_samples, struct call_audio_render* state);
|
||||
|
||||
/* uasync-поток (из call.c): кадр текущего inst/активного DSP передаётся в call_jitter_push;
|
||||
* данные копируются, декодирование на pull. ts_ms — время пачки; packet_start=1 только на первом кадре. Чужой inst/ID или отсутствие DSP — no-op. */
|
||||
void call_audio_on_media(struct UTUN_INSTANCE* inst, uint64_t call_id,
|
||||
|
||||
+34
-11
@@ -3,6 +3,7 @@
|
||||
#include "call_jitter_window.h"
|
||||
#include "call_timeline.h"
|
||||
#include "call_delay.h"
|
||||
#include "call_playout_policy.h"
|
||||
#include <chrono>
|
||||
#include "../../lib/debug_config.h"
|
||||
#include "../../lib/mem.h"
|
||||
@@ -32,7 +33,11 @@ struct call_jitter {
|
||||
call_jitter_diagnostics diagnostics{};
|
||||
int processing_ms = 0, tempo_x100 = 100; /* снимок аудиопотока под mtx */
|
||||
call_jitter_window depth_window, arrival_window;
|
||||
call_jitter_range range = {0, 0, CALL_JITTER_MIN_RESERVE_MS}; /* опубликованный снимок под mtx */
|
||||
call_jitter_window packet_window; // Длительность пачек за то же окно, не второй сетевой запас.
|
||||
int packet_frames = 0;
|
||||
call_playout_policy policy;
|
||||
bool stable_playback = false; // Снимок состояния worker для сетевой оценки, под mtx.
|
||||
call_jitter_range range = {0, 0, CALL_JITTER_STARTUP_MS}; /* опубликованный снимок под mtx */
|
||||
call_jitter_decode_fn decode;
|
||||
void* arg;
|
||||
CallDelay delay{jitter_decode, this, [](void* context, const soundtouch::SourceSpan& span) {
|
||||
@@ -122,8 +127,17 @@ void call_jitter_push(struct call_jitter* j, const uint8_t* enc, int len, uint32
|
||||
(long long)j->arrival_window.last_received_ms, (long long)received_ms);
|
||||
return;
|
||||
}
|
||||
if (packet_start) j->arrival_window.observe_arrival(received_ms, sent_ms);
|
||||
j->range.reserve_ms = j->arrival_window.reserve;
|
||||
if (packet_start) {
|
||||
j->packet_frames = 0;
|
||||
j->arrival_window.observe_arrival(received_ms, sent_ms);
|
||||
int previous = j->policy.reserve_ms;
|
||||
j->policy.observe(received_ms, j->arrival_window.reserve, j->arrival_window.max_reserve, j->stable_playback);
|
||||
if (previous != j->policy.reserve_ms) DEBUG_INFO(DEBUG_CATEGORY_CALL,
|
||||
"call_jitter: reserve=%d->%dms spread=%lldms playing=%d packets=%u", previous, j->policy.reserve_ms,
|
||||
(long long)(j->arrival_window.high - j->arrival_window.low), j->stable_playback, j->policy.packets);
|
||||
}
|
||||
j->packet_window.observe(received_ms, ++j->packet_frames * CALL_JITTER_FRAME_MS);
|
||||
j->range.reserve_ms = j->policy.reserve_ms;
|
||||
if (j->count == CALL_JITTER_MAX_FRAMES) {
|
||||
j->head = (j->head + 1) % CALL_JITTER_MAX_FRAMES; j->count--; j->dropped++;
|
||||
}
|
||||
@@ -134,8 +148,9 @@ void call_jitter_push(struct call_jitter* j, const uint8_t* enc, int len, uint32
|
||||
|
||||
int call_jitter_pull(struct call_jitter* j, int16_t* out, int max_samples) {
|
||||
if (!j || !out || max_samples <= 0) return 0;
|
||||
int count, reserve;
|
||||
{ std::lock_guard<std::mutex> lock(j->mtx); count = j->count; reserve = j->arrival_window.reserve; }
|
||||
int count, reserve, packet_ms;
|
||||
{ std::lock_guard<std::mutex> lock(j->mtx);
|
||||
count = j->count; reserve = j->policy.reserve_ms; packet_ms = (int)j->packet_window.high; }
|
||||
double depth_samples = count * kFrameSamples + j->delay.stats().pending;
|
||||
double depth = depth_samples / 48;
|
||||
int64_t now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
@@ -148,8 +163,7 @@ int call_jitter_pull(struct call_jitter* j, int16_t* out, int max_samples) {
|
||||
}
|
||||
if (!j->primed) {
|
||||
// После первого полного блока остаётся сетевой запас, не скрытый запас DSP.
|
||||
int required = reserve * 48 + max_samples;
|
||||
if (!j->playback_started) required = std::max(required, CALL_JITTER_STARTUP_MS * 48);
|
||||
int required = reserve * 48 + std::max(max_samples, packet_ms * 48);
|
||||
if (depth_samples < required) {
|
||||
std::lock_guard<std::mutex> lock(j->mtx);
|
||||
j->diagnostics.depth_ms = (int)depth; j->diagnostics.reserve_ms = reserve;
|
||||
@@ -173,7 +187,14 @@ int call_jitter_pull(struct call_jitter* j, int16_t* out, int max_samples) {
|
||||
std::lock_guard<std::mutex> lock(j->mtx);
|
||||
j->timeline.read((unsigned)n);
|
||||
j->processing_ms = (int)std::lround(state.pending / 48); j->tempo_x100 = (int)std::lround(state.tempo * 100);
|
||||
if (n < max_samples) j->underruns++;
|
||||
j->stable_playback = n == max_samples;
|
||||
if (n < max_samples) {
|
||||
j->underruns++;
|
||||
j->policy.recover(j->arrival_window.last_received_ms, j->arrival_window.max_reserve);
|
||||
j->range.reserve_ms = j->policy.reserve_ms;
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "call_jitter: rebuffer pcm=%d/%d depth=%.1fms reserve=%dms packet=%dms",
|
||||
n, max_samples, depth, j->policy.reserve_ms, packet_ms);
|
||||
}
|
||||
j->diagnostics.depth_ms = (int)depth;
|
||||
j->diagnostics.reserve_ms = reserve;
|
||||
j->diagnostics.tempo_x100 = j->tempo_x100;
|
||||
@@ -198,7 +219,7 @@ void call_jitter_get_diagnostics(struct call_jitter* j, struct call_jitter_diagn
|
||||
*out = j->diagnostics;
|
||||
out->encoded_ms = j->count * CALL_JITTER_FRAME_MS;
|
||||
out->processing_ms = j->processing_ms;
|
||||
out->reserve_ms = j->arrival_window.reserve;
|
||||
out->reserve_ms = j->policy.reserve_ms;
|
||||
out->arrival_spread_ms = j->arrival_window.high - j->arrival_window.low;
|
||||
out->underruns = j->underruns;
|
||||
if (reset_events) {
|
||||
@@ -242,7 +263,8 @@ int call_jitter_try_clock(struct call_jitter* j, int64_t now_us, struct call_clo
|
||||
void call_jitter_reset(struct call_jitter* j) {
|
||||
if (!j) return;
|
||||
std::lock_guard<std::mutex> lock(j->mtx);
|
||||
int keep = std::min(j->count, (j->arrival_window.reserve + CALL_JITTER_FRAME_MS - 1) / CALL_JITTER_FRAME_MS);
|
||||
j->policy.recover(j->arrival_window.last_received_ms, j->arrival_window.max_reserve);
|
||||
int keep = std::min(j->count, (j->policy.reserve_ms + (int)j->packet_window.high + CALL_JITTER_FRAME_MS - 1) / CALL_JITTER_FRAME_MS);
|
||||
int discard = j->count - keep;
|
||||
j->head = (j->head + discard) % CALL_JITTER_MAX_FRAMES;
|
||||
j->count = keep;
|
||||
@@ -251,12 +273,13 @@ void call_jitter_reset(struct call_jitter* j) {
|
||||
j->timeline.reset(++j->playback_epoch);
|
||||
j->source_epoch = 0; j->capture_end = INT64_MIN;
|
||||
j->primed = false;
|
||||
j->stable_playback = false;
|
||||
j->processing_ms = 0; j->tempo_x100 = 100;
|
||||
j->depth_window = {};
|
||||
j->diagnostics = {};
|
||||
j->range.min_ms = j->range.max_ms = keep * CALL_JITTER_FRAME_MS;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "call_jitter: device discontinuity discarded=%dms kept=%dms reserve=%dms",
|
||||
discard * CALL_JITTER_FRAME_MS, keep * CALL_JITTER_FRAME_MS, j->arrival_window.reserve);
|
||||
discard * CALL_JITTER_FRAME_MS, keep * CALL_JITTER_FRAME_MS, j->policy.reserve_ms);
|
||||
}
|
||||
|
||||
void call_jitter_get_stats(struct call_jitter* j, int* depth_ms, int* tempo_x100,
|
||||
|
||||
@@ -8,15 +8,16 @@
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Звонок: FIFO Opus, запас 20мс..max_reserve_ms по изменению задержки доставки за 10 секунд и догон после паузы устройства.
|
||||
/* Звонок: FIFO Opus и политика качества из call_playout_policy.h.
|
||||
* push — сетевой поток, pull/decode/CallDelay — единственный аудиопоток.
|
||||
* Первый старт ждёт не менее200мс; восстановление — сетевой запас плюс блок; при выдаче PCM запас не резервируется повторно.
|
||||
* Стартовый нижний запас 200мс сохраняется после первого pull и адаптируется по измерениям.
|
||||
* Старт/восстановление ждут запас плюс длительность пачки (не меньше запроса PCM).
|
||||
* destroy — после остановки всех обращений. get_stats/diagnostics/clock — любой поток,
|
||||
* внутренний mutex защищает их снимки. Все операции playback (включая silence/reset)
|
||||
* сериализует владелец; mutex не делает допустимыми два одновременных pull. */
|
||||
#define CALL_JITTER_FRAME_MS 20
|
||||
#define CALL_JITTER_MIN_RESERVE_MS 20
|
||||
#define CALL_JITTER_STARTUP_MS 200 /* Минимальная накопленная речь перед первым декодированием/выводом. */
|
||||
#define CALL_JITTER_STARTUP_MS 200 /* Начальный защищённый запас, сверх периода пачки/запроса PCM. */
|
||||
#define CALL_JITTER_MAX_FRAMES 750 /* 10с перебоя + 5с запаса на накопление и восстановление */
|
||||
/* Заимствованный context, encoded/len и выход/capacity (сэмплы, до 960); вернуть 1..capacity или <=0 при ошибке.
|
||||
* Callback вызывается синхронно на pull без сетевого mutex; encoded действителен только до возврата. */
|
||||
@@ -52,7 +53,7 @@ void call_jitter_push(struct call_jitter* j, const uint8_t* enc, int len, uint32
|
||||
* оставить последние ceil(reserve/20) Opus-кадров. Оценка доставки и суммарные dropped/underruns сохраняются. */
|
||||
void call_jitter_reset(struct call_jitter* j);
|
||||
/* out — max_samples mono int16 @48кГц; вернуть 0..max_samples, без дополнения тишиной.
|
||||
* Первый старт ждёт max(200мс, reserve плюс размер запроса); восстановление — reserve плюс размер запроса.
|
||||
* Старт/восстановление ждут reserve + max(длительность пачки, размер запроса).
|
||||
* Decode ограничен min(750, 8+max_samples/480) попытками.
|
||||
* NULL/неположительный размер дают 0. Вызывающий учитывает дополненную локальную тишину через silence. */
|
||||
int call_jitter_pull(struct call_jitter* j, int16_t* out, int max_samples);
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/* Политика запуска/восстановления звонка. Используется call_jitter под его mutex.
|
||||
*
|
||||
* Владение: сеть добавляет Opus и измерения; единственный playback worker декодирует,
|
||||
* меняет состояние накопления и управляет CallDelay. Адаптер устройства независимо
|
||||
* поддерживает готовый PCM под размер callback. Callback только потребляет очередь.
|
||||
* TX-регулятор пачки независим от RX: отправка не ждёт готовности удалённого вывода.
|
||||
*
|
||||
* reserve_ms — защищённый нижний запас речи, без периода пачки и аппаратного FIFO.
|
||||
* Перед стартом нужен reserve + длительность пачки (не меньше запроса PCM): после
|
||||
* подготовки PCM остаётся запас. Период пачки входит в порог один раз; CallDelay
|
||||
* сравнивает с reserve нижнюю огибающую очереди, а не её пик после получения пачки.
|
||||
* Стартовые 200мс — исходная цель, не одноразовый порог и не максимальная задержка.
|
||||
* Цель растёт сразу, снижается по 20мс после подтверждённой устойчивости. Недостаток
|
||||
* измерений/остановленный выход не являются основанием снижать запас.
|
||||
*
|
||||
* PREBUFFER/RECOVER -> речевой PCM -> подготовленный FIFO адаптера -> реальный вывод.
|
||||
* До первого PCM ожидание отличается от underrun; тишина не считается готовой речью.
|
||||
* CallDelay исполняет reserve, не выбирает его. Задержки FIFO выводятся отдельно;
|
||||
* сумма задержек диагностическая, не общий регулятор всех очередей.
|
||||
* Константы ниже — параметры политики, проверяемые сценариями, не свойства протокола. */
|
||||
#pragma once
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
|
||||
struct call_playout_policy {
|
||||
int reserve_ms = 200; // Нижний сетевой запас, уменьшается только при устойчивом воспроизведении.
|
||||
int64_t stable_since_ms = -1, last_lower_ms = -1;
|
||||
unsigned packets = 0; // Число новых пачек с последнего нарушения устойчивости.
|
||||
static constexpr int StableMs = 5000, LowerIntervalMs = 1000, StepMs = 20;
|
||||
static constexpr unsigned MinimumPackets = 20;
|
||||
|
||||
// Одна оценка на пачку; observed_ms — размах изменения доставки, без постоянного RTT.
|
||||
void observe(int64_t now_ms, int observed_ms, int maximum_ms, bool playing) {
|
||||
int required = std::min(maximum_ms, std::max(20, observed_ms));
|
||||
if (stable_since_ms < 0 || !playing || required > reserve_ms) {
|
||||
stable_since_ms = now_ms; packets = 0;
|
||||
}
|
||||
reserve_ms = std::max(reserve_ms, required);
|
||||
if (packets < MinimumPackets) ++packets;
|
||||
if (playing && packets >= MinimumPackets && now_ms - stable_since_ms >= StableMs
|
||||
&& (last_lower_ms < 0 || now_ms - last_lower_ms >= LowerIntervalMs) && required < reserve_ms) {
|
||||
reserve_ms = std::max(required, reserve_ms - StepMs); last_lower_ms = now_ms;
|
||||
}
|
||||
}
|
||||
// Реальная нехватка/смена устройства отменяет уверенность; повторный старт не расходует старый малый резерв.
|
||||
void recover(int64_t now_ms, int maximum_ms) {
|
||||
reserve_ms = std::min(maximum_ms, std::max(200, reserve_ms + StepMs));
|
||||
stable_since_ms = now_ms; packets = 0; last_lower_ms = now_ms;
|
||||
}
|
||||
};
|
||||
Reference in new issue
Block a user