Сохрани накопленные исправления аудио, async и диагностики
This commit is contained in:
1 parent
b1e3193899
commit
bdcf8b5634
59 files changed
+4611
-1441
No files matched your search
+111
-31
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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 ── */
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user