Files
utun2/tools/chatgui/tests/test_call_jitter.cpp
T
evgeny f521bb37f4 fix(call): align audio frontend ownership with core lifecycle
Keep the borrowed audio provider on UTUN_INSTANCE across chat service restarts.
Attach and detach Android voice from the core worker and reject DSP startup
after detach. Preserve TX owner epochs, pending slots and foreign-instance RX
isolation. Remove the unused cross-thread online API and keep its check local
to call.c.

Add eight lifecycle regressions with real cores and the Android host bridge.
Audit public call, DSP, clock, headless and desktop audio contracts and record
the findings and validation scope.

Validation: voice/desktop 38/38, four native call suites, native and both Android
ABI builds, full check 204/204 plus proxy/burst/load. JVM results remain Gradle
UP-TO-DATE (155 app and 15 router). No hardware runtime validation.
2026-10-26 20:22:46 +01:00

389 lines
20 KiB
C++

// Сохранность FIFO, десятисекундный burst, догон при продолжающемся входе и повторное накопление.
#include "call/call_jitter.h"
#include "call/call_jitter_window.h"
#include "call/call_audio.h"
#include "utun_instance.h"
#include "../../../lib/opus_codec.h"
#include "../../../lib/debug_config.h"
#include <cmath>
#include <cstdio>
#include <cstring>
#include <vector>
static int failures;
static UTUN_INSTANCE audio_owner;
#define CHECK(c) do { if (!(c)) { std::printf("FAIL line %d: %s\n", __LINE__, #c); failures++; } } while (0)
struct decoder { std::vector<int> seen; };
static int decode(void* arg, const uint8_t* enc, int len, int16_t* pcm, int cap) {
auto* d = static_cast<decoder*>(arg);
int id = -1;
if (len == sizeof(id)) std::memcpy(&id, enc, sizeof(id));
d->seen.push_back(id);
for (int i = 0; i < cap; i++) pcm[i] = (int16_t)(8000 * std::sin(2 * 3.141592653589793 * 440 * i / 48000));
return cap;
}
struct broken_decoder { int attempts = 0; };
static int decode_bad(void* arg, const uint8_t*, int, int16_t*, int) {
++static_cast<broken_decoder*>(arg)->attempts;
return -1;
}
static void push(call_jitter* j, int id, int64_t received_ms = -1) {
call_jitter_push(j, (const uint8_t*)&id, sizeof(id), (uint32_t)(id * 20), received_ms < 0 ? id * 20 : received_ms, nullptr);
}
static void test_network_recovery() {
for (int block : {240, 480, 777, 960}) {
decoder d;
auto* j = call_jitter_create(decode, &d, 1000);
std::vector<int16_t> output(block);
int next = 0, largest_reserve = 0;
uint32_t stable_underruns = 0;
call_jitter_diagnostics state{};
for (int64_t time = 0; time < 30 * 48000; time += block) {
// Один перебой 80мс, затем приходит накопленная пачка и продолжается ровная доставка.
bool stalled = time >= 48000 && time < 48000 + 3840;
while (!stalled && next * 960ll <= time) push(j, next++, time / 48);
int n = call_jitter_pull(j, output.data(), block);
if (n < block) call_jitter_silence(j, block - n);
call_jitter_get_diagnostics(j, &state, 0);
largest_reserve = std::max(largest_reserve, state.reserve_ms);
if (time < 3 * 48000) stable_underruns = state.underruns;
else CHECK(state.underruns == stable_underruns);
}
CHECK(largest_reserve >= 60 && state.reserve_ms == 20);
CHECK(state.delay_mode == 0 && state.phase_samples == 0 && state.tempo_ppm == 1000000);
CHECK(state.underruns > 0 && state.underruns <= 2);
DEBUG_INFO(DEBUG_CATEGORY_CALL, "recovery block=%d max_reserve=%dms final=%dms phase=%u underruns=%u",
block, largest_reserve, state.reserve_ms, state.phase_samples, state.underruns);
call_jitter_destroy(j);
}
}
static void test_audio_clock() {
const uint64_t id = 987;
CHECK(call_audio_start(id) == 0);
auto* encoder = opus_codec_encoder_create(48000, 1);
CHECK(encoder);
int delay = 0; CHECK(opus_codec_encoder_delay(encoder, &delay) == 0);
int16_t voice[960], output[480]; uint8_t packet[512];
int sent = 0, valid = 0;
uint64_t written = 0;
int64_t previous = -1;
call_clock_snapshot clock{};
CHECK(call_audio_get_clock(id, 0, &clock) == 0 && !clock.valid);
uint64_t epoch = clock.epoch;
// Начальный burst заставляет настоящий jitter менять темп; PTS не зависит от send_ts.
for (int tick = 0; tick < 4000; tick++) {
int count = tick == 0 ? 30 : (tick % 2 == 0 ? 1 : 0);
for (int i = 0; i < count; i++, sent++) {
for (int s = 0; s < 960; s++) voice[s] = (int16_t)(8000 * std::sin((sent * 960ll + s) * 0.09));
int len = opus_codec_encode(encoder, voice, 960, packet, sizeof(packet)); CHECK(len > 0);
call_capture_time timing{1000000 + sent * 20000ll, 9, (uint32_t)delay};
call_audio_on_media(&audio_owner, id, (uint16_t)sent, 0, packet, len, &timing);
}
CHECK(call_audio_pull_pcm(id, output, 480) == 480); written += 480;
int64_t now = tick * 10000ll;
call_audio_playback_progress(id, epoch, written, now, now, 1);
CHECK(call_audio_get_clock(id, now, &clock) == 0 && clock.written_samples == written);
call_clock_snapshot nonblocking{};
CHECK(call_audio_try_clock(id, now, &nonblocking) == 1);
CHECK(nonblocking.valid == clock.valid && nonblocking.pts_us == clock.pts_us && nonblocking.epoch == clock.epoch);
CHECK(nonblocking.rate_ppm == clock.rate_ppm && nonblocking.written_samples == clock.written_samples);
if (clock.valid) {
valid++;
CHECK(clock.source_epoch == 9 && clock.pts_us >= previous);
CHECK(clock.pts_us <= 1000000 + sent * 20000ll);
CHECK(clock.pts_us > 1000000 + sent * 20000ll - 700000);
previous = clock.pts_us;
}
}
CHECK(valid > 3900);
// Underrun и местные тоны продолжают device-position, но удерживают source-PTS.
int64_t held = -1;
for (int tick = 4000; tick < 4300; tick++) {
CHECK(call_audio_pull_pcm(id, output, 480) == 480); written += 480;
call_audio_playback_progress(id, epoch, written, tick * 10000ll, tick * 10000ll, 1);
CHECK(call_audio_get_clock(id, tick * 10000ll, &clock) == 0 && clock.valid);
if (tick == 4200) held = clock.pts_us;
if (tick > 4200) CHECK(clock.pts_us == held);
}
call_audio_reset_io(id, 1);
CHECK(call_audio_get_clock(id, 43000000, &clock) == 0 && clock.epoch != epoch && !clock.valid);
call_audio_playback_progress(id, epoch, written, 43000000, 43000000, 1);
CHECK(call_audio_get_clock(id, 43000000, &clock) == 0 && !clock.valid);
call_audio_stop(); opus_codec_encoder_destroy(encoder);
CHECK(call_audio_try_clock(id, 43000000, &clock) == -1 && !clock.valid);
std::printf("audio clock: real Opus, burst/tempo, device progress, local tones and reset checked\n");
}
int main() {
CHECK(call_audio_init(&audio_owner) == 0);
{
call_jitter_window w;
w.observe(0, 100); w.observe(500, 400);
CHECK(w.low == 100 && w.high == 400 && w.reserve == 300);
w.observe(1000, 2000); CHECK(w.reserve == 1000);
w.observe(10000, 2000); CHECK(w.low == 2000 && w.reserve == 20);
w.observe(30000, 80); CHECK(w.low == 80 && w.high == 80 && w.reserve == 20);
for (int cap : {500, 2000}) {
call_jitter_window limited;
limited.max_reserve = cap;
limited.observe(0, 0); limited.observe(500, 5000);
CHECK(limited.reserve == cap);
}
}
{
call_jitter_window arrival;
// Разные часы и переполнение uint32 не создают джиттер при ровной доставке.
uint32_t sent = UINT32_MAX - 39;
for (int i = 0; i < 100; i++) arrival.observe_arrival(900000 + i * 20, sent + (uint32_t)(i * 20));
CHECK(arrival.high == 0 && arrival.low == 0 && arrival.reserve == 20);
arrival.observe_arrival(902200, sent + 2000); // задержка доставки +200мс
CHECK(arrival.reserve == 200);
arrival.observe_arrival(902220, sent + 2220); // очередь пришла пачкой
CHECK(arrival.reserve == 200);
arrival.observe_arrival(920000, sent + 20000); // старый burst ушёл из окна
CHECK(arrival.reserve == 20);
// Небольшой джиттер больше минимума тоже учитывается, без прежнего порога 60мс.
arrival.observe_arrival(920055, sent + 20020);
CHECK(arrival.reserve == 35);
arrival.observe_arrival(931055, sent + 31020);
CHECK(arrival.reserve == 20);
}
debug_set_level(DEBUG_LEVEL_WARN);
debug_enable_console(1);
test_audio_clock();
test_network_recovery();
int16_t pcm[480];
int depth, tempo;
uint32_t dropped, under;
{
decoder d;
auto* j = call_jitter_create(decode, &d, 1000);
CHECK(j);
push(j, 0); // Сохраняем запас 20мс после первого блока, а не расходуем его при старте.
CHECK(call_jitter_pull(j, pcm, 480) == 0);
push(j, 1);
CHECK(call_jitter_pull(j, pcm, 480) > 0);
call_jitter_diagnostics diagnostic{};
call_jitter_get_diagnostics(j, &diagnostic, 0);
CHECK(diagnostic.resumed == 1 && diagnostic.pcm_samples == 480 && diagnostic.reserve_ms == 20);
CHECK(diagnostic.encoded_ms >= 0 && diagnostic.processing_ms >= 0 && diagnostic.arrival_spread_ms == 0);
call_jitter_get_diagnostics(j, &diagnostic, 0);
CHECK(diagnostic.resumed == 1); // Снимок playback не потребляет события для uasync.
call_jitter_get_diagnostics(j, &diagnostic, 1);
CHECK(diagnostic.resumed == 1);
call_jitter_get_diagnostics(j, &diagnostic, 0);
CHECK(diagnostic.resumed == 0);
call_jitter_range range;
push(j, 2);
for (int i = 3; i < 503; i++) push(j, i, 10060); // 10с сетевой задержки, затем пачка
call_jitter_get_stats(j, nullptr, nullptr, nullptr, nullptr, &range);
CHECK(range.reserve_ms == 1000);
int next = 503, highest = 100;
for (int tick = 0; tick < 6000; tick++) {
if (tick % 2 == 0) push(j, next++, 10060 + tick * 10); // непрерывный live
call_jitter_pull(j, pcm, 480);
call_jitter_get_stats(j, &depth, &tempo, &dropped, &under, nullptr);
if (tempo > highest) highest = tempo;
}
call_jitter_get_stats(j, nullptr, nullptr, nullptr, nullptr, &range);
CHECK(range.reserve_ms == 20 && range.max_ms >= 10000);
CHECK(range.min_ms <= range.max_ms);
CHECK(highest >= 150);
CHECK(depth < 200);
CHECK(tempo <= 110);
CHECK(dropped == 0);
for (size_t i = 0; i < d.seen.size(); i++) CHECK(d.seen[i] == (int)i);
for (int i = 0; i < 100; i++) call_jitter_pull(j, pcm, 480);
size_t before = d.seen.size();
// После опустошения ждём полный блок сверх сетевого запаса; скрытого накопления DSP больше нет.
push(j, next++);
CHECK(call_jitter_pull(j, pcm, 480) == 0);
CHECK(d.seen.size() == before);
push(j, next++);
int refill = call_jitter_pull(j, pcm, 480);
if (refill != 480) {
call_jitter_get_stats(j, &depth, nullptr, nullptr, nullptr, &range);
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "refill check: pcm=%d remaining=%dms reserve=%dms decoded=%zu",
refill, depth, range.reserve_ms, d.seen.size() - before);
}
CHECK(refill == 480);
CHECK(d.seen.size() == before + 1);
for (int i = 0; i < 50; i++) push(j, next++);
CHECK(call_jitter_pull(j, pcm, 480) > 0);
call_jitter_destroy(j);
}
{
decoder d;
auto* j = call_jitter_create(decode, &d, 1000);
// Устройство готовилось 1.2с, но сеть всё время доставляла кадры равномерно.
for (int i = 0; i < 60; i++) push(j, i);
call_jitter_range range;
call_jitter_get_stats(j, &depth, nullptr, nullptr, nullptr, &range);
CHECK(depth == 1200 && range.reserve_ms == 20);
CHECK(call_jitter_pull(j, pcm, 480) == 480); // первый pull сразу воспроизводит речь
int next = 60, short_pulls = 0;
for (int tick = 0; tick < 6000; tick++) {
if (tick % 2 == 0) push(j, next++);
int produced = call_jitter_pull(j, pcm, 480);
if (produced != 480 && ++short_pulls <= 3) {
call_jitter_diagnostics state{};
call_jitter_get_diagnostics(j, &state, 0);
DEBUG_ERROR(DEBUG_CATEGORY_CALL,
"steady delivery short pull tick=%d pcm=%d depth_before=%d encoded=%d processing=%d reserve=%d tempo=%d spread=%lld",
tick, produced, state.depth_ms, state.encoded_ms, state.processing_ms, state.reserve_ms,
state.tempo_x100, (long long)state.arrival_spread_ms);
}
call_jitter_get_stats(j, &depth, &tempo, &dropped, &under, &range);
CHECK(range.reserve_ms == 20); // догон не увеличивает сетевой резерв
}
CHECK(short_pulls == 0);
CHECK(depth < 150 && tempo <= 110 && dropped == 0 && under == 0);
for (size_t i = 0; i < d.seen.size(); i++) CHECK(d.seen[i] == (int)i);
call_jitter_destroy(j);
}
{
decoder d;
auto* j = call_jitter_create(decode, &d, 1000);
for (int i = 0; i < CALL_JITTER_MAX_FRAMES; i++) push(j, i);
call_jitter_get_stats(j, &depth, nullptr, &dropped, nullptr, nullptr);
CHECK(depth == 15000 && dropped == 0);
push(j, CALL_JITTER_MAX_FRAMES);
call_jitter_get_stats(j, &depth, nullptr, &dropped, nullptr, nullptr);
CHECK(depth == 15000 && dropped == 1);
CHECK(call_jitter_pull(j, pcm, 480) > 0);
CHECK(d.seen.front() == 1);
call_jitter_destroy(j);
}
for (int samples : {240, 480, 960}) {
decoder d;
auto* j = call_jitter_create(decode, &d, 1000);
for (int i = 0; i < 500; i++) push(j, i);
std::vector<int16_t> output(samples);
int elapsed = 0, previous = 100;
for (; elapsed < 500; ) {
CHECK(call_jitter_pull(j, output.data(), samples) == samples);
elapsed += samples * 1000 / 48000;
call_jitter_get_stats(j, nullptr, &tempo, nullptr, nullptr, nullptr);
int expected = (int)std::lround(100 + 60.0 * elapsed / 500);
CHECK(std::abs(tempo - expected) <= 1);
previous = tempo;
}
CHECK(tempo == 160);
bool slowing = false;
for (int tick = 0; tick < 3000 * 960 / samples; tick++) {
call_jitter_pull(j, output.data(), samples);
call_jitter_get_stats(j, &depth, &tempo, &dropped, nullptr, nullptr);
int max_step = (int)std::ceil(120.0 * samples / 48000);
CHECK(std::abs(tempo - previous) <= max_step);
if (tempo < previous) slowing = true;
previous = tempo;
}
CHECK(slowing && tempo <= 110 && dropped == 0);
call_jitter_destroy(j);
}
{
const uint64_t id = 123;
CHECK(call_audio_start(id) == 0);
auto* encoder = opus_codec_encoder_create(48000, 1);
CHECK(encoder);
int16_t voice[960];
for (int i = 0; i < 960; i++) voice[i] = (int16_t)(8000 * std::sin(2 * 3.141592653589793 * 440 * i / 48000));
uint8_t encoded[512];
int len = opus_codec_encode(encoder, voice, 960, encoded, sizeof(encoded));
CHECK(len > 0);
// Подготовка микрофона дольше 500мс не вызывает ложный сигнал потери звука.
for (int tick = 0; tick < 150; tick++) {
CHECK(call_audio_pull_pcm(id, pcm, 480) == 480);
for (auto value : pcm) CHECK(value == 0);
}
// Первый пакет ещё не заполнил запас PCM: продолжается тихое ожидание.
call_audio_on_media(&audio_owner, id, 0, 0, encoded, len, nullptr);
CHECK(call_audio_pull_pcm(id, pcm, 480) == 480);
for (auto value : pcm) CHECK(value == 0);
for (int i = 1; i < 6; i++) call_audio_on_media(&audio_owner, id, i, 0, encoded, len, nullptr);
CHECK(call_audio_pull_pcm(id, pcm, 480) == 480);
bool audible = false;
for (auto value : pcm) if (value) audible = true;
CHECK(audible);
// После начала воспроизведения настоящий перебой по-прежнему включает тон.
audible = false;
for (int tick = 0; tick < 120; tick++) {
CHECK(call_audio_pull_pcm(id, pcm, 480) == 480);
if (tick >= 60) for (auto value : pcm) if (std::abs((int)value) > 1000) audible = true;
}
CHECK(audible);
// Малого запаса недостаточно для восстановления: сигнал продолжает звучать.
call_audio_on_media(&audio_owner, id, 6, 0, encoded, len, nullptr);
audible = false;
for (int tick = 0; tick < 100; tick++) {
CHECK(call_audio_pull_pcm(id, pcm, 480) == 480);
for (auto value : pcm) if (std::abs((int)value) > 1000) audible = true;
}
CHECK(audible);
for (int i = 7; i < 30; i++) call_audio_on_media(&audio_owner, id, i, 0, encoded, len, nullptr);
CHECK(call_audio_pull_pcm(id, pcm, 480) == 480);
call_audio_begin_end(id); // явное завершение прекращает догон и включает финальный тон
CHECK(call_audio_pull_pcm(id, pcm, 480) > 0);
call_audio_release(id);
opus_codec_encoder_destroy(encoder);
}
{
// После PCM порог 500мс работает и для блока, который не делит его нацело.
CHECK(call_audio_start(124) == 0);
auto* encoder = opus_codec_encoder_create(48000, 1);
CHECK(encoder);
int16_t silence[960] = {};
uint8_t encoded[512];
int len = opus_codec_encode(encoder, silence, 960, encoded, sizeof(encoded));
CHECK(len > 0);
for (int i = 0; i < 6; i++) call_audio_on_media(&audio_owner, 124, i, 0, encoded, len, nullptr);
std::vector<int16_t> output(777);
under = 0;
for (int tick = 0; tick < 30 && !under; tick++) {
CHECK(call_audio_pull_pcm(124, output.data(), 777) == 777);
CHECK(call_audio_get_stats(124, nullptr, nullptr, nullptr, &under, nullptr) == 0);
}
CHECK(under > 0);
// Первый недобор занимает не более 777 сэмплов; ещё 29 блоков не достигают 500мс.
for (int tick = 0; tick < 29; tick++) {
CHECK(call_audio_pull_pcm(124, output.data(), 777) == 777);
for (auto value : output) CHECK(std::abs((int)value) <= 5);
}
bool audible = false;
for (int tick = 0; tick < 2; tick++) {
CHECK(call_audio_pull_pcm(124, output.data(), 777) == 777);
for (auto value : output) if (std::abs((int)value) > 1000) audible = true;
}
CHECK(audible);
call_audio_release(124);
opus_codec_encoder_destroy(encoder);
// Новый звонок не наследует включённый тон и отсчёт предыдущего.
CHECK(call_audio_start(125) == 0);
CHECK(call_audio_pull_pcm(125, output.data(), 777) == 777);
for (auto value : output) CHECK(value == 0);
// Завершение звучит даже у звонка, который ещё не получил первого PCM.
call_audio_begin_end(125);
CHECK(call_audio_pull_pcm(125, output.data(), 777) == 777);
audible = false;
for (auto value : output) if (value) audible = true;
CHECK(audible);
call_audio_release(125);
}
{
broken_decoder decoder;
auto* jitter = call_jitter_create(decode_bad, &decoder, 1000);
for (int i = 0; i < CALL_JITTER_MAX_FRAMES; ++i) push(jitter, i);
CHECK(call_jitter_pull(jitter, pcm, 480) == 0);
CHECK(decoder.attempts > 0 && decoder.attempts <= 9);
call_jitter_reset(jitter);
int depth = 0;
call_jitter_get_stats(jitter, &depth, nullptr, nullptr, nullptr, nullptr);
CHECK(depth <= 1000);
call_jitter_destroy(jitter);
}
call_audio_stop(); call_audio_destroy(&audio_owner);
std::printf("call_jitter: %s\n", failures ? "FAILED" : "PASSED");
return failures ? 1 : 0;
}