Сохрани накопленные исправления аудио, async и диагностики

This commit is contained in:
evgeny committed 2026-10-02 01:49:32 +03:00
1 parent b1e3193899
commit bdcf8b5634
59 files changed
+4611 -1441

No files matched your search

+111 -31
View File
@@ -35,7 +35,6 @@ static int g_active = 0;
static int g_ending = 0;
static int g_glitch_started = 0; /* только аудиопоток */
static int g_playback_started = 0; /* первый PCM получен; только аудиопоток */
static uint64_t g_start_tb = 0; /* для диагностики ожидания первого PCM */
static int g_ended_started = 0; /* только аудио-поток: тон завершения запущен */
static uint64_t g_call_id = 0;
static int g_gap_samples = 0; /* только аудиопоток; насыщение на пороге */
@@ -47,6 +46,34 @@ static speex_aec_t* g_aec = NULL;
static int g_aec_delay_frames = CALL_AEC_DELAY_FRAMES;
static int g_aec_enabled = 1; /* по умолчанию: настройка aec_enabled */
static struct audio_compressor* g_compressor = NULL;
static uint64_t g_tx_generation;
static unsigned int g_tx_pending, g_tx_dropped, g_tx_reported;
static unsigned int g_tx_reasons;
static int g_tx_encode_error;
static uint32_t g_aec_reported_over, g_aec_reported_under, g_pcm_events;
/* Task и данные — одна аллокация; uasync освобождает task после callback и при destroy. */
struct call_audio_tx {
struct posted_task task;
struct UTUN_INSTANCE* inst;
uint64_t call_id, generation, created_tb;
int len;
uint8_t opus[256];
};
static void call_audio_send(void* arg) {
struct call_audio_tx* tx = arg;
pthread_mutex_lock(&g_mtx);
int current = tx->generation == g_tx_generation;
if (g_tx_pending) --g_tx_pending;
int send = current && g_active && !g_ending && g_inst == tx->inst && g_call_id == tx->call_id;
if (get_time_tb() - tx->created_tb > 2000) {
send = 0;
if (current) { ++g_tx_dropped; g_tx_reasons |= 16; }
}
pthread_mutex_unlock(&g_mtx);
if (send) call_send_media(tx->inst, tx->call_id, tx->opus, tx->len);
}
static const struct call_audio_ops g_ops = {
.on_media = call_audio_on_media,
@@ -64,8 +91,10 @@ static int call_audio_decode_cb(void* arg, const uint8_t* enc, int len, int16_t*
int call_audio_init(struct UTUN_INSTANCE* inst) {
if (!inst) return -1;
if (g_inst == inst) return 0;
g_inst = inst;
pthread_mutex_lock(&g_mtx);
if (g_inst == inst) { pthread_mutex_unlock(&g_mtx); return 0; }
g_inst = inst; g_tx_pending = 0;
pthread_mutex_unlock(&g_mtx);
call_set_audio_ops(inst, &g_ops);
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: initialized", CALL_AUDIO_ID);
return 0;
@@ -74,7 +103,9 @@ int call_audio_init(struct UTUN_INSTANCE* inst) {
void call_audio_destroy(struct UTUN_INSTANCE* inst) {
if (!inst || g_inst != inst) return;
call_set_audio_ops(inst, NULL);
pthread_mutex_lock(&g_mtx);
g_inst = NULL;
pthread_mutex_unlock(&g_mtx);
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: destroyed", CALL_AUDIO_ID);
}
@@ -183,12 +214,14 @@ int call_audio_start(uint64_t call_id) {
g_compressor = ac;
g_call_id = call_id;
g_active = 1;
++g_tx_generation; g_tx_dropped = g_tx_reported = 0;
g_tx_reasons = 0; g_tx_encode_error = 0;
g_ending = 0;
g_ended_started = 0;
g_glitch_started = 0;
g_playback_started = 0;
g_start_tb = get_time_tb();
g_gap_samples = 0;
g_aec_reported_over = g_aec_reported_under = g_pcm_events = 0;
pthread_mutex_unlock(&g_mtx);
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: started call=0x%016llx aec=%d delay=%d compressor=%s gain=%d", CALL_AUDIO_ID,
@@ -220,10 +253,10 @@ void call_audio_release(uint64_t call_id) {
if (g_active && g_call_id == call_id) {
was_active = 1;
g_active = 0;
++g_tx_generation;
g_ending = 0;
g_ended_started = 0;
g_playback_started = 0;
g_start_tb = 0;
g_call_id = 0;
g_gap_samples = 0;
enc = g_encoder; g_encoder = NULL;
@@ -255,6 +288,23 @@ void call_audio_stop(void) {
if (cid) call_audio_release(cid);
}
void call_audio_reset_io(uint64_t call_id, int playback) {
pthread_mutex_lock(&g_mtx);
if (g_active && g_call_id == call_id) {
if (g_aec) speex_aec_reset(g_aec);
if (playback) {
call_jitter_reset(g_vj);
g_gap_samples = 0;
g_glitch_started = 0;
g_playback_started = 0;
call_tones_glitch_stop(g_tones);
}
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: I/O reset call=%016llx direction=%s", CALL_AUDIO_ID,
(unsigned long long)call_id, playback ? "playback" : "capture");
}
pthread_mutex_unlock(&g_mtx);
}
/* ── AEC (SpeexDSP): рантайм-управление ── */
/* Создать AEC с текущей задержкой. Вызывается под g_mtx. */
@@ -328,21 +378,30 @@ int call_audio_feed_pcm(uint64_t call_id, const int16_t* pcm, int count) {
}
ua = g_inst ? g_inst->ua : NULL;
inst = g_inst;
pthread_mutex_unlock(&g_mtx);
if (len <= 0) return -1;
if (!ua || !inst) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: feed_pcm: instance not set (call_audio_init not called)", CALL_AUDIO_ID);
if (len <= 0 || !ua || !inst || g_ending || g_tx_pending >= 8) {
++g_tx_dropped;
if (g_tx_pending >= 8) g_tx_reasons |= 1;
if (!ua || !inst) g_tx_reasons |= 4;
if (len <= 0) { g_tx_reasons |= 8; g_tx_encode_error = len; }
if (g_ending) g_tx_reasons |= 32;
pthread_mutex_unlock(&g_mtx);
return -1;
}
struct call_media_arg* a = (struct call_media_arg*)u_malloc(sizeof(*a));
if (!a) return -1;
a->inst = inst;
a->call_id = call_id;
a->len = (uint16_t)len;
memcpy(a->opus, opus, (size_t)len);
uasync_post(ua, call_send_media_trampoline, a);
struct call_audio_tx* task = u_calloc(1, sizeof(*task));
if (!task) {
++g_tx_dropped;
g_tx_reasons |= 2;
pthread_mutex_unlock(&g_mtx);
return -1;
}
task->inst = inst; task->call_id = call_id;
task->generation = g_tx_generation; task->created_tb = get_time_tb();
task->len = len;
memcpy(task->opus, opus, (size_t)len);
task->task.callback = call_audio_send; task->task.arg = task;
++g_tx_pending;
uasync_post_reserved(ua, &task->task);
pthread_mutex_unlock(&g_mtx);
return 0;
}
@@ -365,6 +424,7 @@ int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples) {
if (!active || !j || !tones) return 0;
int n = 0;
uint32_t events = 0;
/* завершение: доигрываем тон (450мс), затем тишина. Тон стартует лениво
* здесь (аудио-поток) — begin_end лишь выставляет флаг, чтобы не гонять
@@ -380,9 +440,7 @@ int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples) {
n = call_jitter_pull(j, out, max_samples);
int previous_gap = g_gap_samples;
if (n > 0) {
if (!g_playback_started)
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: first PCM call=%016llx wait=%llums pcm=%d/%d", CALL_AUDIO_ID,
(unsigned long long)call_id, (unsigned long long)((get_time_tb() - g_start_tb) / 10), n, max_samples);
if (!g_playback_started) events |= 1;
g_playback_started = 1;
g_gap_samples = 0;
}
@@ -391,26 +449,20 @@ int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples) {
int quiet = g_playback_started ? CALL_STALL_SAMPLES - g_gap_samples : missing;
if (quiet > missing) quiet = missing;
int tone_samples = missing - quiet;
if (n > 0 && previous_gap > 0)
DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "%s: PCM resumed call=%016llx gap=%dms pcm=%d/%d", CALL_AUDIO_ID,
(unsigned long long)call_id, previous_gap * 1000 / CALL_AUDIO_SAMPLE_RATE, n, max_samples);
if (missing > 0 && g_playback_started && g_gap_samples == 0)
DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "%s: PCM gap started call=%016llx pcm=%d/%d", CALL_AUDIO_ID,
(unsigned long long)call_id, n, max_samples);
if (n > 0 && previous_gap > 0) events |= 2;
if (missing > 0 && g_playback_started && g_gap_samples == 0) events |= 4;
if (g_playback_started) g_gap_samples += quiet;
/* Новый PCM прерывает предыдущий цикл тона, даже если хвост блока снова пуст. */
if (g_glitch_started && (n > 0 || tone_samples == 0)) {
g_glitch_started = 0;
call_tones_glitch_stop(tones);
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: gap tone stopped call=%016llx pcm=%d/%d", CALL_AUDIO_ID,
(unsigned long long)call_id, n, max_samples);
events |= 8;
}
memset(out + n, 0, (size_t)quiet * sizeof(*out));
if (tone_samples > 0) {
if (!g_glitch_started) {
g_glitch_started = 1;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: gap tone started call=%016llx pcm_gap=500ms pcm=%d/%d", CALL_AUDIO_ID,
(unsigned long long)call_id, n, max_samples);
events |= 16;
}
call_tones_glitch_start(tones);
call_tones_render(tones, out + n + quiet, tone_samples);
@@ -422,6 +474,7 @@ int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples) {
if (n > 0) {
pthread_mutex_lock(&g_mtx);
if (g_aec) speex_aec_feed_playback(g_aec, out, n);
g_pcm_events |= events;
pthread_mutex_unlock(&g_mtx);
}
return n;
@@ -432,11 +485,38 @@ int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples) {
int call_audio_get_stats(uint64_t call_id, int* buffer_ms, int* tempo_x100,
uint32_t* dropped, uint32_t* underruns, struct call_jitter_range* range) {
int ok = 0;
struct call_jitter_diagnostics diagnostics = {0};
uint32_t aec_over = 0, aec_under = 0, events = 0;
unsigned int tx_dropped = 0, tx_pending = 0;
unsigned int reasons = 0;
int encode_error = 0;
int tx_changed = 0, aec_changed = 0;
pthread_mutex_lock(&g_mtx);
if (g_active && call_id == g_call_id && g_vj) {
call_jitter_get_stats(g_vj, buffer_ms, tempo_x100, dropped, underruns, range);
call_jitter_take_diagnostics(g_vj, &diagnostics);
speex_aec_get_stats(g_aec, &aec_over, &aec_under);
aec_changed = aec_over != g_aec_reported_over || aec_under != g_aec_reported_under;
g_aec_reported_over = aec_over; g_aec_reported_under = aec_under;
tx_dropped = g_tx_dropped; tx_pending = g_tx_pending;
tx_changed = tx_dropped != g_tx_reported; g_tx_reported = tx_dropped;
reasons = g_tx_reasons; g_tx_reasons = 0; encode_error = g_tx_encode_error;
events = g_pcm_events; g_pcm_events = 0;
ok = 1;
}
pthread_mutex_unlock(&g_mtx);
if (tx_changed) DEBUG_WARN(DEBUG_CATEGORY_CALL,
"%s: TX dropped=%u pending=%u limit=8 max_age=200ms reasons=queue:%d alloc:%d core:%d encode:%d age:%d ending:%d encode_result=%d",
CALL_AUDIO_ID, tx_dropped, tx_pending, !!(reasons & 1), !!(reasons & 2), !!(reasons & 4),
!!(reasons & 8), !!(reasons & 16), !!(reasons & 32), encode_error);
if (aec_changed) DEBUG_WARN(DEBUG_CATEGORY_AEC, "%s: AEC render overflow=%u capture passthrough=%u", CALL_AUDIO_ID, aec_over, aec_under);
if (diagnostics.decode_errors || diagnostics.budget_hits)
DEBUG_WARN(DEBUG_CATEGORY_CALL, "call_jitter: decode_errors=%u last_result=%d len=%d budget_hits=%u depth=%dms",
diagnostics.decode_errors, diagnostics.error_result, diagnostics.error_len, diagnostics.budget_hits, diagnostics.depth_ms);
if (events) DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: PCM first=%d resumed=%d gap=%d tone_started=%d tone_stopped=%d", CALL_AUDIO_ID,
!!(events & 1), !!(events & 2), !!(events & 4), !!(events & 16), !!(events & 8));
if (ok) DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "call_jitter: depth=%dms reserve=%dms tempo=%d waiting=%d catching_up=%d resumed=%d",
diagnostics.depth_ms, diagnostics.reserve_ms, diagnostics.tempo_x100, diagnostics.waiting,
diagnostics.catching_up, diagnostics.resumed);
return ok ? 0 : -1;
}
+6 -1
View File
@@ -12,7 +12,7 @@
//
// Потоки:
// - call_audio_start / begin_end / release — GUI-поток (по ACCEPTED/ENDED);
// - call_audio_feed_pcm / pull_pcm — аудио-поток (single-writer);
// - call_audio_feed_pcm / pull_pcm — по одному аудио-потоку на направление;
// - call_audio_on_media / get_stats — uasync-поток (из call.c).
// Общие указатели (encoder/decoder/vj/tones) защищены мьютексом; encoder и
// decoder — только аудио-поток (decoder дёргается из call_jitter_pull через коллбэк),
@@ -63,6 +63,11 @@ void call_audio_on_media(struct UTUN_INSTANCE* inst, uint64_t call_id,
int call_audio_get_stats(uint64_t call_id, int* buffer_ms, int* tempo_x100,
uint32_t* dropped, uint32_t* underruns, struct call_jitter_range* range);
/* После остановки указанного направления: сбросить AEC, для playback также устаревший jitter/PCM.
* Capture и playback могут работать в разных потоках, по одному writer на направление.
* Release допустим только после остановки обоих. */
void call_audio_reset_io(uint64_t call_id, int playback);
/* Включить/выключить AEC (SpeexDSP) в рантайме. desktop — по настройке GUI,
* Android — по спикерфону (route == SPEAKER). Работает и в активном звонке. */
void call_audio_set_aec_enabled(int enabled);
+55 -21
View File
@@ -30,6 +30,7 @@ struct call_jitter {
int lens[CALL_JITTER_MAX_FRAMES];
int head = 0, count = 0;
uint32_t dropped = 0, underruns = 0;
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, 60}; /* опубликованный снимок под mtx */
@@ -88,16 +89,10 @@ 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;
}
bool log_arrival = j->arrival_window.observe_arrival(received_ms, sent_ms);
j->arrival_window.observe_arrival(received_ms, sent_ms);
j->range.reserve_ms = j->arrival_window.reserve;
if (log_arrival)
DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "call_jitter: delivery variation=%lldms reserve=%dms queued=%dms",
(long long)(j->arrival_window.high - j->arrival_window.low), j->arrival_window.reserve,
j->count * CALL_JITTER_FRAME_MS);
if (j->count == CALL_JITTER_MAX_FRAMES) {
j->head = (j->head + 1) % CALL_JITTER_MAX_FRAMES; j->count--; j->dropped++;
if (j->dropped == 1 || j->dropped % 50 == 0)
DEBUG_WARN(DEBUG_CATEGORY_CALL, "call_jitter: overflow capacity=15000ms dropped=%u", j->dropped);
}
int idx = (j->head + j->count) % CALL_JITTER_MAX_FRAMES;
std::memcpy(j->ring[idx], enc, (size_t)len); j->lens[idx] = len; j->count++;
@@ -112,18 +107,21 @@ int call_jitter_pull(struct call_jitter* j, int16_t* out, int max_samples) {
+ (j->st.numUnprocessedSamples() + j->st.numSamples() * j->tempo) * 1000.0 / kSampleRate;
int64_t now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count();
bool log_stats = j->depth_window.observe(now_ms, (int)std::lround(depth));
j->depth_window.observe(now_ms, (int)std::lround(depth));
int resumed = 0;
{
std::lock_guard<std::mutex> lock(j->mtx);
j->range.min_ms = (int)j->depth_window.low; j->range.max_ms = (int)j->depth_window.high;
}
if (log_stats)
DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "call_jitter: depth=%.0fms min=%lldms max=%lldms reserve=%dms tempo=%.2f",
depth, (long long)j->depth_window.low, (long long)j->depth_window.high, reserve, j->tempo);
if (!j->primed) {
if (depth < reserve) return 0;
if (depth < reserve) {
std::lock_guard<std::mutex> lock(j->mtx);
j->diagnostics.depth_ms = (int)depth; j->diagnostics.reserve_ms = reserve;
j->diagnostics.tempo_x100 = j->tempo_x100; j->diagnostics.waiting = 1;
return 0;
}
j->primed = true;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "call_jitter: playback resumed depth=%.0fms reserve=%dms", depth, reserve);
resumed = 1;
}
j->ema_ms += 0.2 * (depth - j->ema_ms);
double target = 1.0 + (kMaxTempo - 1.0) * clamp_value((std::min(depth, j->ema_ms) - reserve - 15.0) / 925.0, 0.0, 1.0);
@@ -132,13 +130,14 @@ int call_jitter_pull(struct call_jitter* j, int16_t* out, int max_samples) {
j->tempo += clamp_value(target - j->tempo, -step, step);
j->st.setTempo(j->tempo);
bool catching = j->tempo > 1.05;
if (catching != j->catching_up) {
j->catching_up = catching;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "call_jitter: catch-up %s depth=%.0fms tempo=%.2f",
catching ? "started" : "finished", depth, j->tempo);
}
j->catching_up = catching;
uint32_t decode_errors = 0;
int error_result = 0, error_len = 0;
uint8_t enc[kMaxEncoded];
while (j->st.numSamples() < (uint)max_samples) {
// Бюджет учитывает ускорение 1.6 и начальное накопление SoundTouch, включая битые пакеты.
int budget = std::min(CALL_JITTER_MAX_FRAMES, 8 + max_samples / 480);
int attempted = 0;
while (j->st.numSamples() < (uint)max_samples && attempted++ < budget) {
int len;
{
std::lock_guard<std::mutex> lock(j->mtx);
@@ -148,7 +147,8 @@ int call_jitter_pull(struct call_jitter* j, int16_t* out, int max_samples) {
}
int n = j->decode(j->arg, enc, len, j->pcm, kFrameSamples);
if (n <= 0 || n > kFrameSamples) {
DEBUG_WARN(DEBUG_CATEGORY_CALL, "call_jitter: decode failed len=%d result=%d", len, n);
++decode_errors;
error_result = n; error_len = len;
continue;
}
for (int i = 0; i < n; i++) j->input[i] = j->pcm[i] / 32768.0f;
@@ -162,14 +162,48 @@ int call_jitter_pull(struct call_jitter* j, int16_t* out, int max_samples) {
std::lock_guard<std::mutex> lock(j->mtx);
j->processing_ms = processing_ms; j->tempo_x100 = (int)std::lround(j->tempo * 100);
if (n < max_samples) j->underruns++;
j->diagnostics.depth_ms = (int)depth;
j->diagnostics.reserve_ms = reserve;
j->diagnostics.tempo_x100 = j->tempo_x100;
j->diagnostics.decode_errors += decode_errors;
if (decode_errors) { j->diagnostics.error_result = error_result; j->diagnostics.error_len = error_len; }
if (attempted > budget) ++j->diagnostics.budget_hits;
j->diagnostics.resumed |= resumed;
j->diagnostics.catching_up = catching;
j->diagnostics.waiting = n < max_samples;
}
if (n < max_samples) {
j->primed = false;
DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "call_jitter: waiting for refill pcm=%d/%d processing=%dms", n, max_samples, processing_ms);
}
return n;
}
void call_jitter_take_diagnostics(struct call_jitter* j, struct call_jitter_diagnostics* out) {
std::lock_guard<std::mutex> lock(j->mtx);
*out = j->diagnostics;
j->diagnostics.decode_errors = j->diagnostics.budget_hits = 0;
j->diagnostics.resumed = 0;
}
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);
int discard = j->count - keep;
j->head = (j->head + discard) % CALL_JITTER_MAX_FRAMES;
j->count = keep;
j->dropped += discard;
j->st.clear(); j->st.setTempo(1.0);
j->primed = false; j->catching_up = false;
j->tempo = 1.0; j->ema_ms = j->arrival_window.reserve;
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);
}
void call_jitter_get_stats(struct call_jitter* j, int* depth_ms, int* tempo_x100,
uint32_t* dropped, uint32_t* underruns, struct call_jitter_range* range) {
if (!j) return;
+9
View File
@@ -15,11 +15,20 @@ extern "C" {
typedef int (*call_jitter_decode_fn)(void*, const uint8_t*, int, int16_t*, int);
struct call_jitter_range { int min_ms, max_ms, reserve_ms; };
struct call_jitter;
struct call_jitter_diagnostics {
int depth_ms, reserve_ms, tempo_x100, error_result, error_len;
uint32_t decode_errors, budget_hits;
int resumed, catching_up, waiting;
};
/* Снимок и сброс событий под mutex; логировать после выхода из mutex голосового стека. */
void call_jitter_take_diagnostics(struct call_jitter*, struct call_jitter_diagnostics*);
struct call_jitter* call_jitter_create(call_jitter_decode_fn decode, void* arg, int max_reserve_ms);
void call_jitter_destroy(struct call_jitter* j);
/* sent_ms — timestamp кадра на проводе (uint32 с переполнением), received_ms — локальные монотонные часы.
* Абсолютное смещение часов пиров не влияет на оценку джиттера. */
void call_jitter_push(struct call_jitter* j, const uint8_t* enc, int len, uint32_t sent_ms, int64_t received_ms);
/* Только после остановки pull: убрать устаревший PCM, сохранить свежий сетевой запас. */
void call_jitter_reset(struct call_jitter* j);
int call_jitter_pull(struct call_jitter* j, int16_t* out, int max_samples);
void call_jitter_get_stats(struct call_jitter* j, int* depth_ms, int* tempo_x100,
uint32_t* dropped, uint32_t* underruns, struct call_jitter_range* range);
+1 -1
View File
@@ -157,7 +157,7 @@ void chat_core_set_sound_on_message(struct UTUN_INSTANCE* inst, const char* ch_i
struct chat_sound_arg { struct UTUN_INSTANCE* inst; char ch_id[64]; int on; };
void chat_core_set_sound_on_message_trampoline(void* arg);
struct save_ui_state_arg { struct UTUN_INSTANCE* inst; char data[256]; };
struct save_ui_state_arg { struct UTUN_INSTANCE* inst; char data[1024]; };
void chat_core_save_ui_state_trampoline(void* arg);
/* Установка chat-настройки (name=value) через chat_setting API (GUI → uasync) */
+102 -59
View File
@@ -96,11 +96,62 @@ static uint64_t g_last_rx_tb = 0; /* последний п
/* TX-статистика (кадрирование/кодирование), выводится раз в секунду пока идёт PTT. */
static uint64_t g_tx_stats_tb = 0;
static uint64_t g_vad_stats_tb = 0;
static uint32_t g_tx_frames = 0; /* кадров за интервал */
static uint32_t g_tx_bytes = 0; /* суммарный размер opus за интервал */
static uint32_t g_tx_tail = 0; /* дропнутых хвостовых сэмплов за интервал */
/* rj decode-коллбэк: дёргается только из аудио-потока (rj_pull). */
static uint64_t g_tx_generation;
static unsigned int g_tx_pending, g_tx_dropped, g_tx_reported;
struct radio_audio_tx {
struct posted_task task;
struct UTUN_INSTANCE* inst;
uint64_t group_id, generation, created_tb;
int len;
uint8_t opus[RADIO_MAX_OPUS];
};
static void radio_audio_send(void* arg) {
struct radio_audio_tx* tx = arg;
pthread_mutex_lock(&g_mtx);
int current = tx->generation == g_tx_generation;
if (g_tx_pending) --g_tx_pending;
int send = current && g_active && g_inst == tx->inst && g_group_id == tx->group_id;
if (get_time_tb() - tx->created_tb > 2000) { send = 0; if (current) ++g_tx_dropped; }
unsigned int dropped = g_tx_dropped, pending = g_tx_pending;
int changed = dropped != g_tx_reported;
g_tx_reported = dropped;
uint32_t frames = 0, bytes = 0, tail = 0;
uint64_t now = get_time_tb();
if (now - g_tx_stats_tb >= RADIO_AUDIO_STATS_TB) {
frames = g_tx_frames; bytes = g_tx_bytes; tail = g_tx_tail;
g_tx_frames = g_tx_bytes = g_tx_tail = 0; g_tx_stats_tb = now;
}
pthread_mutex_unlock(&g_mtx);
if (changed) {
DEBUG_WARN(DEBUG_CATEGORY_RADIO, "%s: TX dropped=%u pending=%u limit=8 max_age=200ms", RADIO_AUDIO_ID,
dropped, pending);
}
if (frames) DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "%s: TX frames=%u bytes=%u tail_samples=%u", RADIO_AUDIO_ID, frames, bytes, tail);
if (send) radio_talk_send(tx->inst, tx->group_id, tx->opus, tx->len);
}
/* Вызывается под g_mtx; post и FIN упорядочены тем же mutex, задачи не накапливаются бесконечно. */
static int radio_audio_post_frame(struct UTUN_INSTANCE* inst, uint64_t group_id, const uint8_t* opus, int len) {
if (!inst || !inst->ua || g_tx_pending >= 8) { ++g_tx_dropped; return -1; }
struct radio_audio_tx* task = u_calloc(1, sizeof(*task));
if (!task) { ++g_tx_dropped; return -1; }
task->inst = inst; task->group_id = group_id;
task->generation = g_tx_generation; task->created_tb = get_time_tb();
task->len = len;
memcpy(task->opus, opus, (size_t)len);
task->task.callback = radio_audio_send; task->task.arg = task;
++g_tx_pending;
uasync_post_reserved(inst->ua, &task->task);
return 0;
}
/* rj decode-коллбэк: только единственный playback-поток. */
static int radio_audio_decode_cb(void* arg, const uint8_t* enc, int len, int16_t* pcm, int max_samples) {
opus_codec_decoder_t* dec = (opus_codec_decoder_t*)arg;
return opus_codec_decode(dec, enc, len, pcm, max_samples);
@@ -256,14 +307,16 @@ static struct radio_source* radio_src_acquire(uint64_t src, uint16_t stream) {
return NULL;
}
static void radio_audio_post_talk(struct UASYNC* ua, uint64_t group_id, void (*fn)(void*));
static void radio_audio_post_talk(struct UASYNC* ua, uint64_t group_id, int begin);
/* ── lifecycle ── */
int radio_audio_init(struct UTUN_INSTANCE* inst) {
if (!inst) return -1;
if (g_inst == inst) return 0;
g_inst = inst;
pthread_mutex_lock(&g_mtx);
if (g_inst == inst) { pthread_mutex_unlock(&g_mtx); return 0; }
g_inst = inst; g_tx_pending = 0;
pthread_mutex_unlock(&g_mtx);
radio_set_frame_cb(inst, radio_audio_on_frame, NULL);
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: initialized", RADIO_AUDIO_ID);
return 0;
@@ -272,7 +325,9 @@ int radio_audio_init(struct UTUN_INSTANCE* inst) {
void radio_audio_destroy(struct UTUN_INSTANCE* inst) {
if (!inst || g_inst != inst) return;
radio_set_frame_cb(inst, NULL, NULL);
pthread_mutex_lock(&g_mtx);
g_inst = NULL;
pthread_mutex_unlock(&g_mtx);
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: destroyed", RADIO_AUDIO_ID);
}
@@ -348,6 +403,7 @@ int radio_audio_capture_start(uint64_t group_id, int capture_channels, int vad_e
}
g_vad_manual_ptt = 0;
++g_tx_generation; g_tx_dropped = g_tx_reported = 0;
g_encoder = enc;
g_compressor = ac;
g_capture_channels = capture_channels;
@@ -361,8 +417,9 @@ int radio_audio_capture_start(uint64_t group_id, int capture_channels, int vad_e
void radio_audio_capture_stop(void) {
pthread_mutex_lock(&g_mtx);
if (g_encoder) {
++g_tx_generation;
if (g_vad_manual_ptt || g_vad_fsm.talking)
radio_audio_post_talk(g_inst ? g_inst->ua : NULL, g_group_id, radio_talk_end_trampoline);
radio_audio_post_talk(g_inst ? g_inst->ua : NULL, g_group_id, 0);
opus_codec_encoder_destroy(g_encoder); g_encoder = NULL;
if (g_compressor) { audio_compressor_destroy(g_compressor); g_compressor = NULL; }
if (g_vad) { silero_vad_destroy(g_vad); g_vad = NULL; }
@@ -394,6 +451,15 @@ void radio_audio_stop(void) {
}
}
void radio_audio_reset_playback(uint64_t group_id) {
pthread_mutex_lock(&g_mtx);
if (g_active && g_group_id == group_id) {
for (int i = 0; i < RADIO_AUDIO_MAX_SOURCES; ++i) radio_src_free(&g_sources[i]);
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: playback reset group=%016llx", RADIO_AUDIO_ID, (unsigned long long)group_id);
}
pthread_mutex_unlock(&g_mtx);
}
int radio_audio_active(void) {
int a = 0;
pthread_mutex_lock(&g_mtx);
@@ -419,13 +485,26 @@ int radio_audio_transmitting(void) {
/* ── PTT (GUI → uasync) ── */
static void radio_audio_post_talk(struct UASYNC* ua, uint64_t group_id, void (*fn)(void*)) {
struct radio_audio_talk {
struct posted_task task;
struct UTUN_INSTANCE* inst;
uint64_t group_id;
int begin;
};
static void radio_audio_talk(void* arg) {
struct radio_audio_talk* talk = arg;
if (talk->begin) radio_talk_begin(talk->inst, talk->group_id);
else radio_talk_end(talk->inst, talk->group_id);
}
static void radio_audio_post_talk(struct UASYNC* ua, uint64_t group_id, int begin) {
if (!ua) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: talk post without uasync", RADIO_AUDIO_ID); return; }
struct radio_talk_arg* a = (struct radio_talk_arg*)u_malloc(sizeof(*a));
if (!a) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: talk post allocation failed", RADIO_AUDIO_ID); return; }
a->inst = g_inst;
a->group_id = group_id;
uasync_post(ua, fn, a);
struct radio_audio_talk* talk = u_calloc(1, sizeof(*talk));
if (!talk) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: talk post allocation failed", RADIO_AUDIO_ID); return; }
talk->inst = g_inst; talk->group_id = group_id; talk->begin = begin;
talk->task.callback = radio_audio_talk; talk->task.arg = talk;
uasync_post_reserved(ua, &talk->task);
}
void radio_audio_talk_begin(uint64_t group_id) {
@@ -439,10 +518,10 @@ void radio_audio_talk_begin(uint64_t group_id) {
g_vad_manual_ptt = 1;
if (g_vad_fsm.talking) {
/* VAD вела передачу — закрываем её FIN, затем открываем ручной burst */
radio_audio_post_talk(g_inst->ua, group_id, radio_talk_end_trampoline);
radio_audio_post_talk(g_inst->ua, group_id, 0);
radio_vad_fsm_reset(&g_vad_fsm);
}
radio_audio_post_talk(g_inst->ua, group_id, radio_talk_begin_trampoline);
radio_audio_post_talk(g_inst->ua, group_id, 1);
pthread_mutex_unlock(&g_mtx);
}
@@ -456,7 +535,7 @@ void radio_audio_talk_end(uint64_t group_id) {
return;
}
g_vad_manual_ptt = 0;
radio_audio_post_talk(g_inst->ua, group_id, radio_talk_end_trampoline);
radio_audio_post_talk(g_inst->ua, group_id, 0);
pthread_mutex_unlock(&g_mtx);
}
@@ -470,13 +549,7 @@ static void radio_audio_encode_post(uint64_t group_id, const int16_t* frame,
uint8_t opus[RADIO_MAX_OPUS];
int l = opus_codec_encode(g_encoder, frame, RADIO_AUDIO_FRAME_SAMPLES, opus, (int)sizeof(opus));
if (l <= 0) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: opus encode failed rc=%d", RADIO_AUDIO_ID, l); return; }
struct radio_media_arg* a = (struct radio_media_arg*)u_malloc(sizeof(*a));
if (!a) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: media allocation failed", RADIO_AUDIO_ID); return; }
a->inst = inst;
a->group_id = group_id;
a->len = (uint16_t)l;
memcpy(a->opus, opus, (size_t)l);
uasync_post(ua, radio_talk_send_trampoline, a);
radio_audio_post_frame(inst, group_id, opus, l);
}
/* VAD-ветка feed_pcm: ресемпл → Silero → фильтр → старт/стоп/передача (держит g_mtx). */
@@ -517,10 +590,10 @@ static int radio_vad_feed(uint64_t group_id, const int16_t* pcm, int count) {
g_vad_win_len = 0;
uint64_t now = get_time_tb();
int busy = (g_last_rx_tb != 0 && (now - g_last_rx_tb) < RADIO_VAD_BUSY_TB) ? 1 : 0;
if (!g_tx_stats_tb || now - g_tx_stats_tb >= RADIO_AUDIO_STATS_TB) {
if (!g_vad_stats_tb || now - g_vad_stats_tb >= RADIO_AUDIO_STATS_TB) {
DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "%s: vad prob=%.3f threshold=%.2f busy=%d talking=%d confirm=%d",
RADIO_AUDIO_ID, prob, g_vad_threshold, busy, g_vad_fsm.talking, g_vad_fsm.confirm_count);
g_tx_stats_tb = now;
g_vad_stats_tb = now;
}
int action = radio_vad_fsm_update(&g_vad_fsm, prob, g_vad_threshold,
RADIO_VAD_CONFIRM_WINDOWS,
@@ -528,11 +601,11 @@ static int radio_vad_feed(uint64_t group_id, const int16_t* pcm, int count) {
if (action == 1) {
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: vad START prob=%.2f grp=%016llx",
RADIO_AUDIO_ID, prob, (unsigned long long)group_id);
radio_audio_post_talk(ua, group_id, radio_talk_begin_trampoline);
radio_audio_post_talk(ua, group_id, 1);
} else if (action == -1) {
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: vad STOP grp=%016llx",
RADIO_AUDIO_ID, (unsigned long long)group_id);
radio_audio_post_talk(ua, group_id, radio_talk_end_trampoline);
radio_audio_post_talk(ua, group_id, 0);
}
}
}
@@ -557,7 +630,6 @@ int radio_audio_feed_pcm(uint64_t group_id, const int16_t* pcm, int count) {
uint8_t opus[MAX_OUT_FRAMES][RADIO_MAX_OPUS];
int opus_len[MAX_OUT_FRAMES];
int n_frames = 0;
struct UASYNC* ua = NULL;
struct UTUN_INSTANCE* inst = NULL;
if (!g_active || group_id != g_group_id || !g_encoder) {
@@ -589,41 +661,12 @@ int radio_audio_feed_pcm(uint64_t group_id, const int16_t* pcm, int count) {
if (g_compressor) audio_compressor_clear_output(g_compressor);
g_tx_frames += (uint32_t)n_frames;
for (int i = 0; i < n_frames; i++) g_tx_bytes += (uint32_t)opus_len[i];
uint32_t clip = g_compressor ? audio_compressor_clip_count(g_compressor) : 0;
ua = g_inst ? g_inst->ua : NULL;
inst = g_inst;
int result = n_frames ? 0 : -1;
for (int i = 0; i < n_frames; i++)
if (radio_audio_post_frame(inst, group_id, opus[i], opus_len[i]) != 0) result = -1;
pthread_mutex_unlock(&g_mtx);
if (n_frames == 0) return -1;
if (!ua || !inst) {
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: feed_pcm: instance not set", RADIO_AUDIO_ID);
return -1;
}
/* каденс TX: раз в секунду — кадров/с, средний размер opus-кадра, хвост, клиппинг */
uint64_t now = get_time_tb();
if (g_tx_stats_tb == 0 || now - g_tx_stats_tb >= RADIO_AUDIO_STATS_TB) {
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: tx stats frames=%u/s avg=%.0fB/frame tail=%u clip=%u",
RADIO_AUDIO_ID, g_tx_frames,
g_tx_frames ? (double)g_tx_bytes / (double)g_tx_frames : 0.0,
(unsigned)g_tx_tail, (unsigned)clip);
g_tx_stats_tb = now;
g_tx_frames = 0;
g_tx_bytes = 0;
g_tx_tail = 0;
}
for (int i = 0; i < n_frames; i++) {
struct radio_media_arg* a = (struct radio_media_arg*)u_malloc(sizeof(*a));
if (!a) return -1;
a->inst = inst;
a->group_id = group_id;
a->len = (uint16_t)opus_len[i];
memcpy(a->opus, opus[i], (size_t)opus_len[i]);
uasync_post(ua, radio_talk_send_trampoline, a);
}
return 0;
return result;
}
/* ── RX (uasync-поток): кодированный кадр → rj_push ── */
+2
View File
@@ -45,6 +45,8 @@ void radio_audio_capture_stop(void);
/* GUI-поток: прекратить прослушивание (освободить всё). */
void radio_audio_stop(void);
/* Playback остановлен: удалить устаревшие источники, сохранив TX и подписку. */
void radio_audio_reset_playback(uint64_t group_id);
/* GUI-поток: PTT зажата/отпущена — постит radio_talk_begin/end в uasync. */
void radio_audio_talk_begin(uint64_t group_id);