Сохрани накопленные исправления аудио, async и диагностики
This commit is contained in:
1 parent
b1e3193899
commit
bdcf8b5634
59 files changed
+4611
-1441
No files matched your search
@@ -82,10 +82,13 @@ object TelecomCallManager {
|
||||
val tm = telecomManager() ?: return false
|
||||
val handle = phoneAccountHandle ?: return false
|
||||
val uri = Uri.fromParts("node", "0x%016x".format(peerNodeId), null)
|
||||
val extras = Bundle().apply {
|
||||
val callExtras = Bundle().apply {
|
||||
putLong(EXTRA_CALL_ID, callId)
|
||||
putLong(EXTRA_PEER_NODE_ID, peerNodeId)
|
||||
putString(EXTRA_DISPLAY_NAME, displayName)
|
||||
}
|
||||
val extras = Bundle().apply {
|
||||
putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, callExtras)
|
||||
putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, handle)
|
||||
}
|
||||
return try {
|
||||
|
||||
@@ -47,11 +47,17 @@ class UtunConnectionService : ConnectionService() {
|
||||
}
|
||||
|
||||
private fun parseRequest(request: ConnectionRequest?): Triple<Long, Long, String>? {
|
||||
val extras = request?.extras ?: return null
|
||||
val extras = request?.extras ?: run {
|
||||
LogManager.addLog("WARN", "UtunConnSvc", "connection request has no extras")
|
||||
return null
|
||||
}
|
||||
val callId = extras.getLong(TelecomCallManager.EXTRA_CALL_ID, 0L)
|
||||
val peer = extras.getLong(TelecomCallManager.EXTRA_PEER_NODE_ID, 0L)
|
||||
val name = extras.getString(TelecomCallManager.EXTRA_DISPLAY_NAME) ?: ""
|
||||
if (callId == 0L) return null
|
||||
if (callId == 0L) {
|
||||
LogManager.addLog("WARN", "UtunConnSvc", "connection request missing call ID; keys=${extras.keySet()}")
|
||||
return null
|
||||
}
|
||||
return Triple(callId, peer, name)
|
||||
}
|
||||
}
|
||||
@@ -98,14 +98,17 @@ static void bridge_log(int level, const char* fmt, ...) {
|
||||
}
|
||||
|
||||
static void bridge_debug_hook(int level, const char* category, const char* message) {
|
||||
/* DEBUG_*: ERROR=1..TRACE=5; bridge/Kotlin: TRACE=0..ERROR=4. */
|
||||
int bridge_level = level >= DEBUG_LEVEL_ERROR && level <= DEBUG_LEVEL_TRACE ? DEBUG_LEVEL_TRACE - level : BLEV_DEBUG;
|
||||
#ifdef __ANDROID__
|
||||
__android_log_print(blevel_to_android(level), LOG_TAG, "[%s] %s",
|
||||
__android_log_print(blevel_to_android(bridge_level), LOG_TAG, "[%s] %s",
|
||||
category ? category : "?", message ? message : "");
|
||||
#else
|
||||
fprintf(stderr, "[%s] %s\n", category ? category : "?", message ? message : "");
|
||||
#endif
|
||||
udp_log_write(message, (int)strlen(message));
|
||||
if (g_on_log) g_on_log(level, category, message);
|
||||
if (level == DEBUG_LEVEL_ERROR) udp_log_flush();
|
||||
if (g_on_log) g_on_log(bridge_level, category, message);
|
||||
}
|
||||
|
||||
static utun_config_string_fn g_cfg_string = NULL;
|
||||
@@ -1838,7 +1841,6 @@ JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeSetEventCallback(
|
||||
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeDestroy(
|
||||
JNIEnv* env, jobject thiz) {
|
||||
(void)thiz;
|
||||
udp_log_cancel_timer();
|
||||
utun_bridge_destroy();
|
||||
udp_log_stop();
|
||||
if (g_config_callback) { (*env)->DeleteGlobalRef(env, g_config_callback); g_config_callback = NULL; }
|
||||
|
||||
@@ -1,137 +1,177 @@
|
||||
/*
|
||||
* android_udp_log.c — non-blocking UDP log sender for Android
|
||||
*
|
||||
* Called ONLY from uasync worker thread (bridge_debug_hook).
|
||||
* No mutex needed. Flushes at 16KB threshold or via uasync timer.
|
||||
*/
|
||||
/* Android UDP-лог: общий буфер под mutex; таймер принадлежит только uasync-потоку.
|
||||
* ULOG не проходит через debug hook, поэтому ошибки отправки не вызывают рекурсию. */
|
||||
#include "android_udp_log.h"
|
||||
#include "../libutun_lite/instance_lite.h"
|
||||
#include "../../../lib/u_async.h"
|
||||
#include "../../../lib/platform_compat.h"
|
||||
#include "../../../lib/socket_compat.h"
|
||||
#include "../../../lib/mem.h"
|
||||
#include <pthread.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <errno.h>
|
||||
|
||||
#define UDP_BUF_SIZE 65536
|
||||
#define UDP_BUF_SIZE 65507 /* Максимальный UDP payload для IPv4. */
|
||||
#define UDP_FLUSH_BYTES 16384
|
||||
#define UDP_FLUSH_TB 2000 /* 200ms in uasync timebase (0.1ms units) */
|
||||
#define UDP_FLUSH_TB 2000
|
||||
|
||||
#ifdef __ANDROID__
|
||||
#include <android/log.h>
|
||||
#define ULOG(fmt, ...) __android_log_print(ANDROID_LOG_DEBUG, "utun-udplog", fmt, ##__VA_ARGS__)
|
||||
#define ULOG(fmt, ...) __android_log_print(ANDROID_LOG_INFO, "utun-udplog", fmt, ##__VA_ARGS__)
|
||||
#else
|
||||
#define ULOG(fmt, ...) fprintf(stderr, "utun-udplog: " fmt "\n", ##__VA_ARGS__)
|
||||
#define ULOG(fmt, ...) fprintf(stderr, "utun-udplog: " fmt "\n", ##__VA_ARGS__)
|
||||
#endif
|
||||
|
||||
static int g_udp_fd = -1;
|
||||
static pthread_mutex_t g_udp_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
static struct UASYNC* g_udp_ua;
|
||||
static int g_udp_fd = -1;
|
||||
static struct sockaddr_in g_udp_addr;
|
||||
static int g_udp_has_addr = 0;
|
||||
static int g_udp_stopping = 0;
|
||||
static char g_udp_buf[UDP_BUF_SIZE];
|
||||
static int g_udp_buf_len = 0;
|
||||
static void* g_udp_timer = NULL;
|
||||
static int g_udp_sent_first = 0;
|
||||
static int64_t g_udp_total_sent = 0;
|
||||
static int g_udp_send_errors = 0;
|
||||
|
||||
static void udp_reschedule_timer(void) {
|
||||
if (g_udp_stopping) return;
|
||||
if (g_udp_timer) return;
|
||||
struct UASYNC* ua = instance_lite_get_uasync();
|
||||
if (ua) g_udp_timer = uasync_set_timeout(ua, UDP_FLUSH_TB, NULL, udp_trampoline, "udp_log_flush");
|
||||
}
|
||||
|
||||
static void udp_open_socket(void) {
|
||||
if (g_udp_fd >= 0 || !g_udp_has_addr) return;
|
||||
g_udp_fd = (int)socket(AF_INET, SOCK_DGRAM | SOCK_NONBLOCK, 0);
|
||||
if (g_udp_fd < 0) { ULOG("socket() failed: %s (errno=%d)", strerror(errno), errno); return; }
|
||||
if (connect(g_udp_fd, (struct sockaddr*)&g_udp_addr, sizeof(g_udp_addr)) < 0) {
|
||||
ULOG("connect() to %s:%d failed: %s (errno=%d)",
|
||||
inet_ntoa(g_udp_addr.sin_addr), (int)ntohs(g_udp_addr.sin_port), strerror(errno), errno);
|
||||
close(g_udp_fd); g_udp_fd = -1; return;
|
||||
}
|
||||
ULOG("socket opened fd=%d -> %s:%d", g_udp_fd,
|
||||
inet_ntoa(g_udp_addr.sin_addr), (int)ntohs(g_udp_addr.sin_port));
|
||||
}
|
||||
static int g_udp_has_addr;
|
||||
static int g_udp_stopping;
|
||||
static char g_udp_buf[UDP_BUF_SIZE];
|
||||
static int g_udp_buf_len;
|
||||
static int g_udp_sent_first;
|
||||
static int64_t g_udp_total_sent;
|
||||
static unsigned g_udp_send_errors;
|
||||
/* Только uasync-поток читает и меняет handle. JNI и log hook его не трогают. */
|
||||
static void* g_udp_timer;
|
||||
|
||||
/* Вызывается под g_udp_mutex; все операции сокета неблокирующие. */
|
||||
static void udp_do_send(const char* data, int len) {
|
||||
if (g_udp_fd < 0) { udp_open_socket(); if (g_udp_fd < 0) return; }
|
||||
if (g_udp_fd < 0) {
|
||||
g_udp_fd = (int)socket(AF_INET, SOCK_DGRAM | SOCK_NONBLOCK, 0);
|
||||
if (g_udp_fd < 0) { ULOG("socket failed: %s (errno=%d)", strerror(errno), errno); return; }
|
||||
if (connect(g_udp_fd, (struct sockaddr*)&g_udp_addr, sizeof(g_udp_addr)) < 0) {
|
||||
ULOG("connect failed: %s (errno=%d)", strerror(errno), errno);
|
||||
close(g_udp_fd); g_udp_fd = -1; return;
|
||||
}
|
||||
}
|
||||
ssize_t sent = send(g_udp_fd, data, len, MSG_DONTWAIT);
|
||||
if (sent >= 0) {
|
||||
if (sent == len) {
|
||||
g_udp_total_sent += sent;
|
||||
if (!g_udp_sent_first) { ULOG("first send OK: %zd bytes, total=%lld", sent, (long long)g_udp_total_sent); g_udp_sent_first = 1; }
|
||||
if (!g_udp_sent_first) { ULOG("first send OK: %zd bytes", sent); g_udp_sent_first = 1; }
|
||||
return;
|
||||
}
|
||||
int send_errno = sent < 0 ? errno : 0;
|
||||
g_udp_send_errors++;
|
||||
if (g_udp_send_errors <= 3 || g_udp_send_errors % 100 == 0)
|
||||
ULOG("send() error #%d: %s (errno=%d)", g_udp_send_errors, strerror(errno), errno);
|
||||
if (errno == ECONNREFUSED || errno == ENETUNREACH || errno == EHOSTUNREACH) {
|
||||
close(g_udp_fd);
|
||||
g_udp_fd = -1;
|
||||
ULOG("send error #%u: sent=%zd expected=%d error=%s (errno=%d)",
|
||||
g_udp_send_errors, sent, len, send_errno ? strerror(send_errno) : "short datagram", send_errno);
|
||||
if (send_errno == ECONNREFUSED || send_errno == ENETUNREACH || send_errno == EHOSTUNREACH) {
|
||||
close(g_udp_fd); g_udp_fd = -1;
|
||||
}
|
||||
}
|
||||
|
||||
void udp_trampoline(void* arg) {
|
||||
(void)arg;
|
||||
if (g_udp_buf_len > 0) {
|
||||
udp_do_send(g_udp_buf, g_udp_buf_len);
|
||||
g_udp_buf_len = 0;
|
||||
}
|
||||
static void udp_flush_locked(void) {
|
||||
if (!g_udp_buf_len) return;
|
||||
udp_do_send(g_udp_buf, g_udp_buf_len);
|
||||
g_udp_buf_len = 0;
|
||||
}
|
||||
|
||||
static void udp_update_timer(void* arg);
|
||||
|
||||
static void udp_flush_timer(void* arg) {
|
||||
g_udp_timer = NULL;
|
||||
/* перевзводим только если остались неотправленные данные */
|
||||
if (g_udp_buf_len > 0) udp_reschedule_timer();
|
||||
pthread_mutex_lock(&g_udp_mutex);
|
||||
if (g_udp_ua == arg && g_udp_has_addr && !g_udp_stopping) udp_flush_locked();
|
||||
pthread_mutex_unlock(&g_udp_mutex);
|
||||
udp_update_timer(arg);
|
||||
}
|
||||
|
||||
/* Только uasync-поток: attach или posted-команда смены адреса. Mutex отпускаем
|
||||
* до операций с таймером: их DEBUG_ERROR может снова вызвать udp_log_write. */
|
||||
static void udp_update_timer(void* arg) {
|
||||
struct UASYNC* ua = arg;
|
||||
pthread_mutex_lock(&g_udp_mutex);
|
||||
int owns = g_udp_ua == ua;
|
||||
int enabled = g_udp_has_addr && !g_udp_stopping;
|
||||
pthread_mutex_unlock(&g_udp_mutex);
|
||||
if (!owns) return;
|
||||
if (!enabled) {
|
||||
if (g_udp_timer) uasync_cancel_timeout(ua, g_udp_timer);
|
||||
g_udp_timer = NULL;
|
||||
} else if (!g_udp_timer) {
|
||||
g_udp_timer = uasync_set_timeout(ua, UDP_FLUSH_TB, ua, udp_flush_timer, "udp_log_flush");
|
||||
if (!g_udp_timer) ULOG("flush timer allocation failed ua=%p", (void*)ua);
|
||||
}
|
||||
}
|
||||
|
||||
void udp_log_attach(struct UASYNC* ua) {
|
||||
if (!ua) { ULOG("attach rejected: ua=NULL"); return; }
|
||||
pthread_mutex_lock(&g_udp_mutex);
|
||||
if (g_udp_ua) {
|
||||
ULOG("attach rejected: owner=%p requested=%p", (void*)g_udp_ua, (void*)ua);
|
||||
pthread_mutex_unlock(&g_udp_mutex);
|
||||
return;
|
||||
}
|
||||
g_udp_ua = ua;
|
||||
pthread_mutex_unlock(&g_udp_mutex);
|
||||
udp_update_timer(ua);
|
||||
ULOG("attached ua=%p: flush timer owned by core thread", (void*)ua);
|
||||
}
|
||||
|
||||
void udp_log_detach(struct UASYNC* ua) {
|
||||
if (!ua) return;
|
||||
pthread_mutex_lock(&g_udp_mutex);
|
||||
if (g_udp_ua != ua) { pthread_mutex_unlock(&g_udp_mutex); return; }
|
||||
g_udp_ua = NULL; /* После этого ни один producer не может post в старый ua. */
|
||||
pthread_mutex_unlock(&g_udp_mutex);
|
||||
if (g_udp_timer) uasync_cancel_timeout(ua, g_udp_timer);
|
||||
g_udp_timer = NULL;
|
||||
ULOG("detached ua=%p: flush timer cancelled", (void*)ua);
|
||||
}
|
||||
|
||||
void udp_log_set_target(const char* ip, int port) {
|
||||
g_udp_buf_len = 0;
|
||||
g_udp_stopping = 0;
|
||||
pthread_mutex_lock(&g_udp_mutex);
|
||||
if (g_udp_has_addr) udp_flush_locked();
|
||||
if (g_udp_fd >= 0) { close(g_udp_fd); g_udp_fd = -1; }
|
||||
memset(&g_udp_addr, 0, sizeof(g_udp_addr));
|
||||
g_udp_addr.sin_family = AF_INET;
|
||||
g_udp_has_addr = ip && port > 0 && port <= 65535 && inet_pton(AF_INET, ip, &g_udp_addr.sin_addr) == 1;
|
||||
g_udp_addr.sin_port = htons((unsigned short)(g_udp_has_addr ? port : 0));
|
||||
g_udp_stopping = !g_udp_has_addr;
|
||||
g_udp_sent_first = 0;
|
||||
g_udp_total_sent = 0;
|
||||
g_udp_send_errors = 0;
|
||||
if (g_udp_fd >= 0) { close(g_udp_fd); g_udp_fd = -1; }
|
||||
if (ip && port > 0) {
|
||||
memset(&g_udp_addr, 0, sizeof(g_udp_addr));
|
||||
g_udp_addr.sin_family = AF_INET;
|
||||
g_udp_addr.sin_port = htons((unsigned short)port);
|
||||
g_udp_has_addr = (inet_pton(AF_INET, ip, &g_udp_addr.sin_addr) == 1);
|
||||
ULOG("set target %s:%d -> has_addr=%d", ip, port, g_udp_has_addr);
|
||||
} else {
|
||||
g_udp_has_addr = 0;
|
||||
ULOG("set target disabled (ip=%s port=%d)", ip ? ip : "null", port);
|
||||
if (g_udp_ua) {
|
||||
/* Маленький u_malloc не логирует; при отказе используем только ULOG.
|
||||
* post_reserved тоже не вызывает debug hook. Detach ждёт этот mutex. */
|
||||
struct posted_task* task = u_malloc(sizeof(*task));
|
||||
if (task) {
|
||||
task->callback = udp_update_timer;
|
||||
task->arg = g_udp_ua;
|
||||
uasync_post_reserved(g_udp_ua, task);
|
||||
} else ULOG("target changed but timer update allocation failed");
|
||||
}
|
||||
ULOG("target %s:%d enabled=%d", ip ? ip : "(null)", port, g_udp_has_addr);
|
||||
pthread_mutex_unlock(&g_udp_mutex);
|
||||
}
|
||||
|
||||
void udp_log_write(const char* data, int len) {
|
||||
if (!data || len <= 0 || !g_udp_has_addr) return;
|
||||
|
||||
if (g_udp_fd < 0) udp_open_socket();
|
||||
|
||||
if (g_udp_buf_len + len > UDP_BUF_SIZE) {
|
||||
if (g_udp_buf_len > 0) { udp_do_send(g_udp_buf, g_udp_buf_len); g_udp_buf_len = 0; }
|
||||
if (len > UDP_BUF_SIZE) return;
|
||||
}
|
||||
memcpy(g_udp_buf + g_udp_buf_len, data, len);
|
||||
g_udp_buf_len += len;
|
||||
if (g_udp_buf_len >= UDP_FLUSH_BYTES) {
|
||||
udp_do_send(g_udp_buf, g_udp_buf_len);
|
||||
g_udp_buf_len = 0;
|
||||
if (!data || len <= 0) return;
|
||||
pthread_mutex_lock(&g_udp_mutex);
|
||||
if (!g_udp_has_addr || g_udp_stopping) { pthread_mutex_unlock(&g_udp_mutex); return; }
|
||||
if (len > UDP_BUF_SIZE) {
|
||||
ULOG("log dropped: %d bytes exceeds UDP payload limit %d", len, UDP_BUF_SIZE);
|
||||
} else {
|
||||
udp_reschedule_timer(); /* взвести flush для частичных данных */
|
||||
if (len > UDP_BUF_SIZE - g_udp_buf_len) udp_flush_locked();
|
||||
memcpy(g_udp_buf + g_udp_buf_len, data, len);
|
||||
g_udp_buf_len += len;
|
||||
if (g_udp_buf_len >= UDP_FLUSH_BYTES) udp_flush_locked();
|
||||
}
|
||||
pthread_mutex_unlock(&g_udp_mutex);
|
||||
}
|
||||
|
||||
void udp_log_cancel_timer(void) {
|
||||
g_udp_stopping = 1;
|
||||
struct UASYNC* ua = instance_lite_get_uasync();
|
||||
if (ua && g_udp_timer) uasync_cancel_timeout(ua, g_udp_timer);
|
||||
g_udp_timer = NULL;
|
||||
void udp_log_flush(void) {
|
||||
pthread_mutex_lock(&g_udp_mutex);
|
||||
if (g_udp_has_addr) udp_flush_locked();
|
||||
pthread_mutex_unlock(&g_udp_mutex);
|
||||
}
|
||||
|
||||
void udp_log_stop(void) {
|
||||
pthread_mutex_lock(&g_udp_mutex);
|
||||
g_udp_stopping = 1;
|
||||
if (g_udp_buf_len > 0) { udp_do_send(g_udp_buf, g_udp_buf_len); g_udp_buf_len = 0; }
|
||||
ULOG("stop: total_sent=%lld bytes, errors=%d", (long long)g_udp_total_sent, g_udp_send_errors);
|
||||
if (g_udp_has_addr) udp_flush_locked();
|
||||
ULOG("stop: total_sent=%lld bytes errors=%u", (long long)g_udp_total_sent, g_udp_send_errors);
|
||||
if (g_udp_fd >= 0) { close(g_udp_fd); g_udp_fd = -1; }
|
||||
g_udp_has_addr = 0;
|
||||
pthread_mutex_unlock(&g_udp_mutex);
|
||||
}
|
||||
@@ -11,22 +11,25 @@
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Configure target. ua may be NULL (delayed init, will retry on write).
|
||||
* Call from ANY thread. Posts timer setup to uasync event loop. */
|
||||
struct UASYNC;
|
||||
|
||||
/* Только поток ядра: attach после create, detach перед каждым destroy/restart. */
|
||||
void udp_log_attach(struct UASYNC* ua);
|
||||
void udp_log_detach(struct UASYNC* ua);
|
||||
|
||||
/* Любой поток. Смена адреса передаёт управление таймером в поток ядра. */
|
||||
void udp_log_set_target(const char* ip, int port);
|
||||
|
||||
/* Write log line to buffer. Non-blocking send if threshold reached.
|
||||
* Thread-safe — call from any thread (bridge_debug_hook). */
|
||||
void udp_log_write(const char* data, int len);
|
||||
|
||||
/* Cancel flush timer (call before uasync_destroy, while ua is alive). */
|
||||
void udp_log_cancel_timer(void);
|
||||
/* Любой поток: ошибки отправляем немедленно, до возможной остановки ядра. */
|
||||
void udp_log_flush(void);
|
||||
|
||||
/* Flush buffer, close socket. Call after instance shutdown. */
|
||||
void udp_log_stop(void);
|
||||
|
||||
void udp_trampoline(void* arg);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -276,9 +276,8 @@ static void* instance_thread(void* arg) {
|
||||
if (!g_ua) { IL_LOGE("uasync_create failed"); free_config(config); __atomic_store_n(&g_thread_running, 0, __ATOMIC_RELEASE); pthread_detach(pthread_self()); return NULL; }
|
||||
standby_init(g_ua);
|
||||
|
||||
if (config->global.log_udp_ip[0] && config->global.log_udp_port > 0) {
|
||||
udp_log_set_target(config->global.log_udp_ip, config->global.log_udp_port);
|
||||
}
|
||||
udp_log_set_target(config->global.log_udp_ip, config->global.log_udp_port);
|
||||
udp_log_attach(g_ua);
|
||||
|
||||
IL_LOGI("[EVT_DIAG] thread: about to set chat_event handler, g_event_handler=%p", (void*)g_event_handler);
|
||||
ensure_keys(config);
|
||||
@@ -288,6 +287,7 @@ static void* instance_thread(void* arg) {
|
||||
if (!g_inst) {
|
||||
IL_LOGE("utun_instance_create_from_config failed");
|
||||
standby_deinit();
|
||||
udp_log_detach(g_ua);
|
||||
uasync_destroy(g_ua, 0);
|
||||
g_ua = NULL;
|
||||
u_report_unfreed_blocks();
|
||||
@@ -305,6 +305,7 @@ static void* instance_thread(void* arg) {
|
||||
utun_instance_destroy(g_inst);
|
||||
g_inst = NULL;
|
||||
standby_deinit();
|
||||
udp_log_detach(g_ua);
|
||||
uasync_destroy(g_ua, 0);
|
||||
g_ua = NULL;
|
||||
u_report_unfreed_blocks();
|
||||
@@ -362,6 +363,7 @@ static void* instance_thread(void* arg) {
|
||||
uasync_print_resources(g_ua, "AFTER_DESTROY");
|
||||
u_report_unfreed_blocks();
|
||||
standby_deinit();
|
||||
udp_log_detach(g_ua);
|
||||
uasync_destroy(g_ua, 0);
|
||||
my_ua = NULL;
|
||||
g_ua = uasync_create();
|
||||
@@ -378,6 +380,8 @@ static void* instance_thread(void* arg) {
|
||||
u_free(cfg);
|
||||
if (!config) { IL_LOGE("poll exit: parse_config failed"); break; }
|
||||
config->global.client_type = CLIENT_TYPE_MOBILE; /* Android — всегда мобильный узел */
|
||||
udp_log_set_target(config->global.log_udp_ip, config->global.log_udp_port);
|
||||
udp_log_attach(g_ua);
|
||||
|
||||
g_inst = utun_instance_create_from_config(g_ua, config);
|
||||
my_inst = g_inst;
|
||||
@@ -414,6 +418,7 @@ static void* instance_thread(void* arg) {
|
||||
}
|
||||
if (my_ua) {
|
||||
standby_deinit();
|
||||
udp_log_detach(my_ua);
|
||||
uasync_destroy(my_ua, 0);
|
||||
g_ua = NULL;
|
||||
my_ua = NULL;
|
||||
@@ -428,7 +433,7 @@ static void* instance_thread(void* arg) {
|
||||
IL_LOGI("cleanup (stale thread gen=%d vs %d): destroying own inst=%p ua=%p to avoid fd leak",
|
||||
my_gen, g_generation, (void*)my_inst, (void*)my_ua);
|
||||
if (my_inst) { utun_instance_destroy(my_inst); my_inst = NULL; }
|
||||
if (my_ua) { uasync_destroy(my_ua, 0); my_ua = NULL; }
|
||||
if (my_ua) { udp_log_detach(my_ua); uasync_destroy(my_ua, 0); my_ua = NULL; }
|
||||
}
|
||||
__atomic_store_n(&g_thread_running, 0, __ATOMIC_RELEASE);
|
||||
pthread_mutex_lock(&g_stop_mutex);
|
||||
|
||||
@@ -130,6 +130,7 @@ add_executable(vibechat
|
||||
src/nodespage.cpp
|
||||
src/soundsettingspage.cpp
|
||||
src/sound_manager.cpp
|
||||
src/audio_device.cpp
|
||||
src/audiorecorder.cpp
|
||||
src/voicemessageencoder.cpp
|
||||
src/voiceplayback.cpp
|
||||
@@ -150,6 +151,7 @@ add_executable(vibechat
|
||||
transport/config_updater.cpp
|
||||
transport/gui_bridge_impl.cpp
|
||||
transport/miniaudio_impl.c
|
||||
transport/audio_diagnostics.cpp
|
||||
db/db_manager.cpp
|
||||
../../lib/sqlite3.c
|
||||
resources/chatgui.qrc
|
||||
@@ -244,7 +246,9 @@ add_executable(test_video_engine
|
||||
tests/test_video_engine.cpp
|
||||
src/videoplayer_engine.cpp
|
||||
src/sound_manager.cpp
|
||||
src/audio_device.cpp
|
||||
transport/miniaudio_impl.c
|
||||
transport/audio_diagnostics.cpp
|
||||
)
|
||||
target_include_directories(test_video_engine PRIVATE ${CMAKE_SOURCE_DIR}/../../lib ${CMAKE_SOURCE_DIR}/../../src ${CMAKE_SOURCE_DIR}/db ${CMAKE_SOURCE_DIR}/../../lib/libopus/include ${FFMPEG_INCLUDE_DIRS})
|
||||
target_compile_definitions(test_video_engine PRIVATE USE_SQLITE SQLITE_THREADSAFE=1)
|
||||
@@ -262,12 +266,21 @@ add_executable(test_ptt_key_chord tests/test_ptt_key_chord.cpp)
|
||||
target_link_libraries(test_ptt_key_chord PRIVATE ${QT_CORE})
|
||||
add_test(NAME test_ptt_key_chord COMMAND test_ptt_key_chord)
|
||||
|
||||
add_executable(test_voice_file tests/test_voice_file.cpp src/voiceplayback.cpp src/sound_manager.cpp transport/miniaudio_impl.c)
|
||||
add_executable(test_voice_file tests/test_voice_file.cpp src/voiceplayback.cpp src/sound_manager.cpp src/audio_device.cpp transport/miniaudio_impl.c transport/audio_diagnostics.cpp)
|
||||
target_include_directories(test_voice_file PRIVATE ${UTUN_INCLUDE_DIRS})
|
||||
target_link_libraries(test_voice_file PRIVATE ${QT_LIBS} utun pthread ${CMAKE_DL_LIBS})
|
||||
add_test(NAME test_voice_file COMMAND test_voice_file)
|
||||
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
add_executable(test_audio_recovery tests/test_audio_recovery.cpp src/audio_device.cpp src/sound_manager.cpp
|
||||
src/audiorecorder.cpp src/call_audio_engine.cpp src/radio_audio_engine.cpp
|
||||
transport/miniaudio_impl.c transport/audio_diagnostics.cpp)
|
||||
target_include_directories(test_audio_recovery PRIVATE ${UTUN_INCLUDE_DIRS})
|
||||
target_link_libraries(test_audio_recovery PRIVATE ${QT_LIBS} utun_voice utun pthread dl)
|
||||
target_link_options(test_audio_recovery PRIVATE "-Wl,--wrap=ma_context_init" "-Wl,--wrap=ma_context_uninit"
|
||||
"-Wl,--wrap=ma_device_init" "-Wl,--wrap=ma_device_start" "-Wl,--wrap=ma_device_uninit" "-Wl,--wrap=audio_context_failed")
|
||||
add_test(NAME test_audio_recovery COMMAND test_audio_recovery)
|
||||
set_tests_properties(test_audio_recovery PROPERTIES TIMEOUT 15)
|
||||
add_executable(test_ptt_portal tests/test_ptt_portal.cpp src/ptt_portal.cpp)
|
||||
target_link_libraries(test_ptt_portal PRIVATE ${QT_DBUS} ${QT_TEST_LIB} utun pthread)
|
||||
find_program(DBUS_RUN_SESSION dbus-run-session)
|
||||
@@ -275,4 +288,15 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
message(FATAL_ERROR "dbus-run-session is required for the isolated PTT portal test")
|
||||
endif()
|
||||
add_test(NAME test_ptt_portal COMMAND ${DBUS_RUN_SESSION} -- $<TARGET_FILE:test_ptt_portal>)
|
||||
|
||||
add_executable(test_linux_audio tests/test_linux_audio.c transport/audio_diagnostics.cpp)
|
||||
target_link_libraries(test_linux_audio PRIVATE utun pthread dl m)
|
||||
# assert выполняет вызовы стенда, поэтому не отключаем его в Release.
|
||||
target_compile_options(test_linux_audio PRIVATE -UNDEBUG)
|
||||
add_test(NAME test_linux_audio COMMAND test_linux_audio)
|
||||
|
||||
add_executable(test_audio_diagnostics tests/test_audio_diagnostics.cpp)
|
||||
target_link_libraries(test_audio_diagnostics PRIVATE utun pthread)
|
||||
target_compile_options(test_audio_diagnostics PRIVATE -UNDEBUG)
|
||||
add_test(NAME test_audio_diagnostics COMMAND test_audio_diagnostics)
|
||||
endif()
|
||||
@@ -0,0 +1,96 @@
|
||||
#include "audio_device.h"
|
||||
#include "../transport/audio_diagnostics.h"
|
||||
#include "debug_config.h"
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
||||
AudioDevice::AudioDevice() { m_clock.start(); }
|
||||
AudioDevice::~AudioDevice() { close(); }
|
||||
|
||||
bool AudioDevice::init(ma_context* context, ma_device_config config, const char* role, uint64_t session, Callback callback) {
|
||||
close();
|
||||
m_role = role;
|
||||
m_callback = std::move(callback);
|
||||
if (!context) { retryLater(); return false; }
|
||||
auto* device = new ma_device{};
|
||||
config.dataCallback = data;
|
||||
config.notificationCallback = notification;
|
||||
config.pUserData = this;
|
||||
ma_result result = ma_device_init(context, &config, device);
|
||||
if (result != MA_SUCCESS) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio: %s init failed result=%d", role, result);
|
||||
delete device;
|
||||
retryLater();
|
||||
return false;
|
||||
}
|
||||
m_device = device;
|
||||
m_frames = 0;
|
||||
m_seenFrames = 0;
|
||||
m_suspended = false;
|
||||
audio_diag_label(device, session, role);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "audio: %s initialized backend=%s rate=%u capture='%s' playback='%s' period=%u/%u",
|
||||
role, ma_get_backend_name(context->backend), device->sampleRate, device->capture.name, device->playback.name,
|
||||
device->capture.internalPeriodSizeInFrames, device->playback.internalPeriodSizeInFrames);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AudioDevice::start() {
|
||||
if (!m_device) return false;
|
||||
m_progressAt = m_clock.elapsed();
|
||||
ma_result result = ma_device_start(m_device);
|
||||
if (result != MA_SUCCESS) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio: %s start failed result=%d", m_role, result);
|
||||
close();
|
||||
retryLater();
|
||||
return false;
|
||||
}
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "audio: %s started", m_role);
|
||||
return true;
|
||||
}
|
||||
|
||||
void AudioDevice::close() {
|
||||
if (m_device) {
|
||||
ma_device_uninit(m_device); /* uninit включает stop/join, в том числе после неудачного start. */
|
||||
delete m_device;
|
||||
m_device = nullptr;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "audio: %s closed", m_role);
|
||||
}
|
||||
m_callback = {};
|
||||
}
|
||||
|
||||
void AudioDevice::data(ma_device* device, void* out, const void* in, ma_uint32 frames) {
|
||||
auto* self = static_cast<AudioDevice*>(device->pUserData);
|
||||
if (self->m_callback) self->m_callback(in, out, frames);
|
||||
self->m_frames.fetch_add(frames, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void AudioDevice::notification(const ma_device_notification* event) {
|
||||
auto* self = static_cast<AudioDevice*>(event->pDevice->pUserData);
|
||||
if (event->type == ma_device_notification_type_interruption_began) self->m_suspended = true;
|
||||
if (event->type == ma_device_notification_type_interruption_ended) self->m_suspended = false;
|
||||
}
|
||||
|
||||
bool AudioDevice::needsRecovery() {
|
||||
if (!m_device) return false;
|
||||
uint64_t frames = m_frames.load(std::memory_order_relaxed);
|
||||
qint64 now = m_clock.elapsed();
|
||||
if (frames != m_seenFrames || m_suspended.load()) {
|
||||
m_seenFrames = frames;
|
||||
m_progressAt = now;
|
||||
if (frames) m_retryDelay = 250;
|
||||
}
|
||||
bool stopped = ma_device_get_state(m_device) == ma_device_state_stopped;
|
||||
if (!stopped && now - m_progressAt < 1000) return false;
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "audio: %s recovery reason=%s idle=%lldms frames=%llu suspended=%d",
|
||||
m_role, stopped ? "stopped" : "no-progress", (long long)(now - m_progressAt),
|
||||
(unsigned long long)frames, m_suspended.load());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AudioDevice::retryReady() const { return !m_device && m_clock.elapsed() >= m_retryAt; }
|
||||
void AudioDevice::retryLater() {
|
||||
m_retryAt = m_clock.elapsed() + m_retryDelay;
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "audio: %s retry in %dms", m_role, m_retryDelay);
|
||||
m_retryDelay = std::min(m_retryDelay * 2, 2000);
|
||||
}
|
||||
void AudioDevice::retryNow() { m_retryAt = 0; m_retryDelay = 250; }
|
||||
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include "miniaudio.h"
|
||||
#include <QElapsedTimer>
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
|
||||
/* Один владелец в GUI-потоке. Callback публикует только прогресс и уведомления;
|
||||
* close() дожидается worker перед освобождением callback и его состояния. */
|
||||
class AudioDevice {
|
||||
public:
|
||||
using Callback = std::function<void(const void*, void*, unsigned int)>;
|
||||
AudioDevice();
|
||||
~AudioDevice();
|
||||
AudioDevice(const AudioDevice&) = delete;
|
||||
AudioDevice& operator=(const AudioDevice&) = delete;
|
||||
bool init(ma_context* context, ma_device_config config, const char* role, uint64_t session, Callback callback);
|
||||
bool start();
|
||||
void close();
|
||||
bool needsRecovery();
|
||||
bool retryReady() const;
|
||||
void retryLater();
|
||||
void retryNow();
|
||||
bool initialized() const { return m_device != nullptr; }
|
||||
ma_device* device() const { return m_device; }
|
||||
private:
|
||||
static void data(ma_device* device, void* out, const void* in, ma_uint32 frames);
|
||||
static void notification(const ma_device_notification* event);
|
||||
ma_device* m_device = nullptr;
|
||||
Callback m_callback;
|
||||
const char* m_role = "audio";
|
||||
QElapsedTimer m_clock;
|
||||
std::atomic<uint64_t> m_frames{0};
|
||||
std::atomic<bool> m_suspended{false};
|
||||
uint64_t m_seenFrames = 0;
|
||||
qint64 m_progressAt = 0, m_retryAt = 0;
|
||||
int m_retryDelay = 250;
|
||||
};
|
||||
@@ -183,25 +183,25 @@ void AudioDeviceSettingsPage::loadFromDb() {
|
||||
|
||||
/* Output devices */
|
||||
m_outputCombo->clear();
|
||||
m_outputCombo->addItem("Default (auto)", -1);
|
||||
m_outputCombo->addItem("Default (auto)", QString());
|
||||
for (int i = 0; i < playbackDevs.size(); i++) {
|
||||
QString label = playbackDevs[i].name;
|
||||
if (playbackDevs[i].isDefault) label += " (default)";
|
||||
m_outputCombo->addItem(label, i);
|
||||
m_outputCombo->addItem(label, playbackDevs[i].id);
|
||||
}
|
||||
|
||||
/* Input devices */
|
||||
m_inputCombo->clear();
|
||||
m_inputCombo->addItem("Default (auto)", -1);
|
||||
m_inputCombo->addItem("Default (auto)", QString());
|
||||
for (int i = 0; i < captureDevs.size(); i++) {
|
||||
QString label = captureDevs[i].name;
|
||||
if (captureDevs[i].isDefault) label += " (default)";
|
||||
m_inputCombo->addItem(label, i);
|
||||
m_inputCombo->addItem(label, captureDevs[i].id);
|
||||
}
|
||||
|
||||
if (m_db) {
|
||||
m_savedOutputIdx = m_db->getUiStateInt("audio_output_device", -1);
|
||||
m_savedInputIdx = m_db->getUiStateInt("audio_input_device", -1);
|
||||
m_savedOutputId = m_db->getUiState("audio_output_id");
|
||||
m_savedInputId = m_db->getUiState("audio_input_id");
|
||||
m_savedCodecPreset = m_db->getUiStateInt("opus_codec_preset", 1);
|
||||
m_savedRadioOpusMode = m_db->getUiStateInt("radio_opus_mode", 0);
|
||||
m_savedRadioOpusBitrate = m_db->getUiStateInt("radio_opus_bitrate", 32);
|
||||
@@ -215,11 +215,11 @@ void AudioDeviceSettingsPage::loadFromDb() {
|
||||
|
||||
/* Select saved indices */
|
||||
for (int i = 0; i < m_outputCombo->count(); i++) {
|
||||
if (m_outputCombo->itemData(i).toInt() == m_savedOutputIdx)
|
||||
if (m_outputCombo->itemData(i).toString() == m_savedOutputId)
|
||||
{ m_outputCombo->setCurrentIndex(i); break; }
|
||||
}
|
||||
for (int i = 0; i < m_inputCombo->count(); i++) {
|
||||
if (m_inputCombo->itemData(i).toInt() == m_savedInputIdx)
|
||||
if (m_inputCombo->itemData(i).toString() == m_savedInputId)
|
||||
{ m_inputCombo->setCurrentIndex(i); break; }
|
||||
}
|
||||
for (int i = 0; i < m_codecPresetCombo->count(); i++) {
|
||||
@@ -244,8 +244,8 @@ void AudioDeviceSettingsPage::loadFromDb() {
|
||||
}
|
||||
|
||||
void AudioDeviceSettingsPage::applyAndSave() {
|
||||
int outIdx = m_outputCombo->currentData().toInt();
|
||||
int inIdx = m_inputCombo->currentData().toInt();
|
||||
QString outputId = m_outputCombo->currentData().toString();
|
||||
QString inputId = m_inputCombo->currentData().toString();
|
||||
int codecPreset = m_codecPresetCombo->currentData().toInt();
|
||||
|
||||
auto saveKey = [](const char* key, int val) {
|
||||
@@ -260,8 +260,23 @@ void AudioDeviceSettingsPage::applyAndSave() {
|
||||
memcpy(arg->data + klen + 1, v.constData(), vlen + 1);
|
||||
gui_bridge_post_uasync_fn(chat_core_save_ui_state_trampoline, arg);
|
||||
};
|
||||
saveKey("audio_output_device", outIdx);
|
||||
saveKey("audio_input_device", inIdx);
|
||||
auto saveDevice = [](const char* key, const QString& id) {
|
||||
QByteArray value = id.toUtf8();
|
||||
size_t keyLen = strlen(key), valueLen = value.size();
|
||||
auto* arg = static_cast<save_ui_state_arg*>(u_calloc(1, sizeof(save_ui_state_arg)));
|
||||
if (!arg) return;
|
||||
if (keyLen + valueLen + 2 > sizeof(arg->data)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio: device ID too long bytes=%zu", valueLen);
|
||||
u_free(arg); return;
|
||||
}
|
||||
arg->inst = gui_bridge_get_inst();
|
||||
memcpy(arg->data, key, keyLen + 1);
|
||||
memcpy(arg->data + keyLen + 1, value.constData(), valueLen + 1);
|
||||
gui_bridge_post_uasync_fn(chat_core_save_ui_state_trampoline, arg);
|
||||
};
|
||||
saveDevice("audio_output_id", outputId);
|
||||
saveDevice("audio_input_id", inputId);
|
||||
SoundManager::instance()->setDevices(outputId, inputId);
|
||||
saveKey("opus_codec_preset", codecPreset);
|
||||
|
||||
VoiceEncoder::setPreset(codecPreset);
|
||||
@@ -312,8 +327,8 @@ void AudioDeviceSettingsPage::applyAndSave() {
|
||||
postChat(m_configPath, "call_jitter_max_reserve_ms", jitterReserveMs);
|
||||
}
|
||||
|
||||
m_savedOutputIdx = outIdx;
|
||||
m_savedInputIdx = inIdx;
|
||||
m_savedOutputId = outputId;
|
||||
m_savedInputId = inputId;
|
||||
m_savedCodecPreset = codecPreset;
|
||||
m_savedRadioOpusMode = radioOpusMode;
|
||||
m_savedRadioOpusBitrate = radioOpusBitrate;
|
||||
@@ -322,13 +337,13 @@ void AudioDeviceSettingsPage::applyAndSave() {
|
||||
m_savedCompressorMaxGainDb = compMaxGain;
|
||||
m_savedCompressorRiseRate = compRiseRate;
|
||||
m_savedAecEnabled = aecEnabled;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "saved output=%d input=%d codec=%d radioOpus=%d/%dkbps stereo=%d compressor=%d gain=%ddB rise=%.1fx aec=%d",
|
||||
outIdx, inIdx, codecPreset, radioOpusMode, radioOpusBitrate, radioCaptureStereo,
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "saved output_default=%d input_default=%d codec=%d radioOpus=%d/%dkbps stereo=%d compressor=%d gain=%ddB rise=%.1fx aec=%d",
|
||||
outputId.isEmpty(), inputId.isEmpty(), codecPreset, radioOpusMode, radioOpusBitrate, radioCaptureStereo,
|
||||
compEnabled, compMaxGain, (float)compRiseRate, aecEnabled);
|
||||
|
||||
if (m_recorder) {
|
||||
m_recorder->setCompressorEnabled(compEnabled != 0);
|
||||
if (m_recorder->compressor() && compEnabled) {
|
||||
{
|
||||
audio_compressor_config_t compCfg = {0};
|
||||
compCfg.sample_rate = 48000;
|
||||
compCfg.channels = 1;
|
||||
@@ -337,7 +352,7 @@ void AudioDeviceSettingsPage::applyAndSave() {
|
||||
compCfg.lookahead_ms = 100;
|
||||
compCfg.rise_rate_per_sec = (float)compRiseRate;
|
||||
compCfg.target_level = 1.0f;
|
||||
audio_compressor_configure(m_recorder->compressor(), &compCfg);
|
||||
m_recorder->configureCompressor(compCfg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -346,8 +361,8 @@ void AudioDeviceSettingsPage::onTestMicToggled() {
|
||||
if (m_testMicBtn->isChecked()) {
|
||||
m_testRecorder = new AudioRecorder(this);
|
||||
m_testRecorder->init();
|
||||
int inIdx = m_inputCombo->currentData().toInt();
|
||||
m_testRecorder->setCaptureDeviceIndex(inIdx);
|
||||
QString id = m_inputCombo->currentData().toString();
|
||||
m_testRecorder->setCaptureDevice(id);
|
||||
m_testRecorder->startRecording();
|
||||
m_micTestActive = true;
|
||||
m_levelTimer->start();
|
||||
|
||||
@@ -41,8 +41,8 @@ private:
|
||||
QPushButton* m_testSpeakerBtn;
|
||||
QTimer* m_levelTimer;
|
||||
bool m_micTestActive = false;
|
||||
int m_savedOutputIdx = -1;
|
||||
int m_savedInputIdx = -1;
|
||||
QString m_savedOutputId;
|
||||
QString m_savedInputId;
|
||||
int m_savedCodecPreset = 1;
|
||||
|
||||
QCheckBox* m_compressorEnabledCb;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "media_async/attachment_send.h"
|
||||
#include "miniaudio.h"
|
||||
#include "sound_manager.h"
|
||||
#include "../transport/audio_diagnostics.h"
|
||||
|
||||
extern "C" {
|
||||
#include "../../lib/audio_compressor.h"
|
||||
@@ -11,57 +12,60 @@ extern "C" {
|
||||
#include <cstring>
|
||||
#include <cstdlib>
|
||||
#include <cmath>
|
||||
static AudioRecorder* g_recorder = nullptr;
|
||||
|
||||
void audioCaptureCallback(ma_device* pDevice, void* pOutput, const void* pInput, unsigned int frameCount) {
|
||||
(void)pOutput;
|
||||
AudioRecorder* self = g_recorder;
|
||||
if (!self || !self->m_recording) return;
|
||||
if (!pInput) return;
|
||||
|
||||
void AudioRecorder::captureCallback(const void* pInput, unsigned int frameCount) {
|
||||
if (!pInput || !frameCount || !m_recording.load()) return;
|
||||
const int16_t* src = (const int16_t*)pInput;
|
||||
size_t add = (size_t)frameCount * (size_t)self->m_channels;
|
||||
size_t add = (size_t)frameCount * (size_t)m_channels;
|
||||
|
||||
if (self->m_compressor && self->m_compressorEnabled) {
|
||||
audio_compressor_push(self->m_compressor, src, add);
|
||||
if (m_compressor && m_captureCompressorEnabled) {
|
||||
audio_compressor_push(m_compressor, src, add);
|
||||
} else {
|
||||
size_t cur = self->m_pcmBuffer.size();
|
||||
self->m_pcmBuffer.resize(cur + add);
|
||||
std::memcpy(self->m_pcmBuffer.data() + cur, src, add * sizeof(int16_t));
|
||||
size_t cur = m_pcmBuffer.size();
|
||||
m_pcmBuffer.resize(cur + add);
|
||||
std::memcpy(m_pcmBuffer.data() + cur, src, add * sizeof(int16_t));
|
||||
}
|
||||
|
||||
float sumSq = 0.0f;
|
||||
for (unsigned int i = 0; i < frameCount * (unsigned int)self->m_channels; i++) {
|
||||
for (unsigned int i = 0; i < frameCount * (unsigned int)m_channels; i++) {
|
||||
float v = (float)src[i] / 32768.0f;
|
||||
sumSq += v * v;
|
||||
}
|
||||
float rms = sqrtf(sumSq / (float)(frameCount * self->m_channels));
|
||||
self->m_peakLevel = rms;
|
||||
float rms = sqrtf(sumSq / (float)(frameCount * m_channels));
|
||||
m_peakLevel = rms;
|
||||
|
||||
self->m_recentRms[self->m_rmsIdx % 5] = rms;
|
||||
self->m_rmsIdx++;
|
||||
if (self->m_rmsIdx >= 5) {
|
||||
float avg = 0.0f;
|
||||
for (int i = 0; i < 5; i++) avg += self->m_recentRms[i];
|
||||
avg /= 5.0f;
|
||||
self->m_waveformLevels.push_back(avg);
|
||||
}
|
||||
m_recordedFrames.fetch_add(frameCount, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
AudioRecorder::AudioRecorder(QObject* parent)
|
||||
: QObject(parent) {
|
||||
g_recorder = this;
|
||||
m_compressor = audio_compressor_create();
|
||||
if (m_compressor) {
|
||||
audio_compressor_config_t cfg = {0};
|
||||
cfg.sample_rate = m_sampleRate;
|
||||
cfg.channels = m_channels;
|
||||
m_compressorConfig = cfg;
|
||||
audio_compressor_configure(m_compressor, &cfg);
|
||||
}
|
||||
m_captureId = SoundManager::instance()->captureDevice();
|
||||
connect(SoundManager::instance(), &SoundManager::devicesChanged, this, [this]() {
|
||||
QString id = SoundManager::instance()->captureDevice();
|
||||
if (!m_customDevice && id != m_captureId) {
|
||||
m_captureId = id; m_capture.close(); m_capture.retryNow(); recoverCapture();
|
||||
}
|
||||
});
|
||||
m_recoveryTimer.setInterval(200);
|
||||
connect(&m_recoveryTimer, &QTimer::timeout, this, &AudioRecorder::recoverCapture);
|
||||
connect(SoundManager::instance(), &SoundManager::contextAboutToReset, this, [this]() {
|
||||
m_capture.close(); m_durationTimer.stop();
|
||||
});
|
||||
connect(SoundManager::instance(), &SoundManager::contextRestored, this, [this]() {
|
||||
m_capture.retryNow(); recoverCapture();
|
||||
});
|
||||
m_durationTimer.setInterval(100);
|
||||
connect(&m_durationTimer, &QTimer::timeout, this, [this]() {
|
||||
if (m_recording) {
|
||||
m_elapsedMs += 100;
|
||||
m_elapsedMs = (int)(m_recordedFrames.load(std::memory_order_relaxed) * 1000 / m_sampleRate);
|
||||
emit durationChanged(m_elapsedMs);
|
||||
}
|
||||
});
|
||||
@@ -71,103 +75,71 @@ AudioRecorder::~AudioRecorder() {
|
||||
shutdown();
|
||||
audio_compressor_destroy(m_compressor);
|
||||
m_compressor = nullptr;
|
||||
if (g_recorder == this) g_recorder = nullptr;
|
||||
}
|
||||
|
||||
bool AudioRecorder::init() {
|
||||
if (m_initialized) return true;
|
||||
m_device = new ma_device;
|
||||
std::memset(m_device, 0, sizeof(*m_device));
|
||||
m_initialized = true;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "initialized (sampleRate=%d, channels=%d)", m_sampleRate, m_channels);
|
||||
return true;
|
||||
}
|
||||
|
||||
void AudioRecorder::shutdown() {
|
||||
if (!m_initialized) return;
|
||||
if (m_recording) stopRecording();
|
||||
if (m_deviceStarted) {
|
||||
m_deviceStarted = false;
|
||||
if (m_device) { ma_device_stop(m_device); ma_device_uninit(m_device); }
|
||||
}
|
||||
if (m_device) { delete m_device; m_device = nullptr; }
|
||||
stopRecording();
|
||||
m_recoveryTimer.stop();
|
||||
m_capture.close();
|
||||
m_initialized = false;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "shutdown");
|
||||
}
|
||||
|
||||
void AudioRecorder::startRecording() {
|
||||
if (m_recording) return;
|
||||
if (!m_initialized) { DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "not initialized"); return; }
|
||||
|
||||
if (!m_initialized) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "recorder: not initialized"); return; }
|
||||
m_pcmBuffer.clear();
|
||||
m_waveformLevels.clear();
|
||||
m_rmsIdx = 0;
|
||||
m_elapsedMs = 0;
|
||||
|
||||
if (m_compressor) audio_compressor_reset(m_compressor);
|
||||
|
||||
ma_context* ctx = SoundManager::instance()->context();
|
||||
if (!ctx) { DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "no audio context"); return; }
|
||||
|
||||
ma_device_config config = ma_device_config_init(ma_device_type_capture);
|
||||
config.capture.format = ma_format_s16;
|
||||
config.capture.channels = (ma_uint32)m_channels;
|
||||
config.sampleRate = (ma_uint32)m_sampleRate;
|
||||
config.periodSizeInFrames = 480;
|
||||
config.dataCallback = audioCaptureCallback;
|
||||
config.pUserData = this;
|
||||
|
||||
if (m_captureDeviceIndex >= 0) {
|
||||
ma_device_info* pCaptureDevices = nullptr;
|
||||
ma_uint32 captureCount = 0;
|
||||
ma_result r = ma_context_get_devices(ctx, nullptr, nullptr, &pCaptureDevices, &captureCount);
|
||||
if (r == MA_SUCCESS && (ma_uint32)m_captureDeviceIndex < captureCount) {
|
||||
config.capture.pDeviceID = &pCaptureDevices[m_captureDeviceIndex].id;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "using capture device[%d]: %s", m_captureDeviceIndex,
|
||||
pCaptureDevices[m_captureDeviceIndex].name);
|
||||
}
|
||||
m_recordedFrames = 0; m_elapsedMs = 0; m_peakLevel = 0;
|
||||
m_captureCompressorEnabled = m_compressorEnabled;
|
||||
if (m_compressor) {
|
||||
audio_compressor_configure(m_compressor, &m_compressorConfig);
|
||||
audio_compressor_set_enabled(m_compressor, m_captureCompressorEnabled);
|
||||
audio_compressor_reset(m_compressor);
|
||||
}
|
||||
|
||||
ma_result r = ma_device_init(ctx, &config, m_device);
|
||||
if (r != MA_SUCCESS) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "device init failed (%d), trying default", r);
|
||||
config.capture.pDeviceID = nullptr;
|
||||
r = ma_device_init(ctx, &config, m_device);
|
||||
if (r != MA_SUCCESS) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "default device init also failed (%d)", r);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
r = ma_device_start(m_device);
|
||||
if (r != MA_SUCCESS) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "device start failed (%d)", r);
|
||||
ma_device_uninit(m_device);
|
||||
return;
|
||||
}
|
||||
|
||||
m_recording = true;
|
||||
m_deviceStarted = true;
|
||||
m_durationTimer.start();
|
||||
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "recording started");
|
||||
m_capture.retryNow();
|
||||
recoverCapture();
|
||||
m_recoveryTimer.start();
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "recorder: recording requested compressor=%d", m_captureCompressorEnabled);
|
||||
emit recordingStarted();
|
||||
}
|
||||
|
||||
void AudioRecorder::stopRecording() {
|
||||
void AudioRecorder::recoverCapture() {
|
||||
if (!m_recording) return;
|
||||
|
||||
m_durationTimer.stop();
|
||||
if (m_deviceStarted) {
|
||||
ma_device_stop(m_device);
|
||||
ma_device_uninit(m_device);
|
||||
m_deviceStarted = false;
|
||||
if (m_capture.needsRecovery()) { m_capture.close(); m_durationTimer.stop(); m_capture.retryLater(); }
|
||||
if (!m_capture.retryReady()) return;
|
||||
ma_context* context = SoundManager::instance()->context();
|
||||
if (!context) return;
|
||||
ma_device_config config = ma_device_config_init(ma_device_type_capture);
|
||||
config.capture.format = ma_format_s16;
|
||||
config.capture.channels = m_channels;
|
||||
config.sampleRate = m_sampleRate;
|
||||
ma_device_id id;
|
||||
bool selected = SoundManager::deviceId(m_captureId, &id);
|
||||
if (selected) config.capture.pDeviceID = &id;
|
||||
auto callback = [this](const void* input, void*, unsigned int frames) { captureCallback(input, frames); };
|
||||
bool ready = m_capture.init(context, config, "voice-recorder", 0, callback) && m_capture.start();
|
||||
if (!ready) {
|
||||
if (!selected) return;
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "recorder: selected input unavailable, trying default");
|
||||
config.capture.pDeviceID = nullptr;
|
||||
ready = m_capture.init(context, config, "voice-recorder", 0, callback) && m_capture.start();
|
||||
}
|
||||
m_recording = false;
|
||||
if (ready) m_durationTimer.start();
|
||||
}
|
||||
|
||||
void AudioRecorder::stopRecording() {
|
||||
if (!m_recording.exchange(false)) return;
|
||||
m_recoveryTimer.stop(); m_durationTimer.stop();
|
||||
m_capture.close();
|
||||
m_elapsedMs = (int)(m_recordedFrames.load() * 1000 / m_sampleRate);
|
||||
int duration = m_elapsedMs;
|
||||
size_t bufSize = (m_compressor && m_compressorEnabled) ? audio_compressor_output_size(m_compressor) : m_pcmBuffer.size();
|
||||
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "recording stopped, duration=%dms, pcmSamples=%zu, compressor=%d",
|
||||
duration, bufSize, m_compressorEnabled ? 1 : 0);
|
||||
size_t samples = m_compressor && m_captureCompressorEnabled ? audio_compressor_output_size(m_compressor) : m_pcmBuffer.size();
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "recorder: stopped duration=%dms samples=%zu", duration, samples);
|
||||
emit recordingStopped(duration);
|
||||
emit durationChanged(0);
|
||||
}
|
||||
@@ -179,7 +151,7 @@ int AudioRecorder::durationMs() const {
|
||||
/* Worker получает владельца буфера; новая запись имеет отдельное состояние. */
|
||||
bool AudioRecorder::takeRecording(struct attachment_send_req* request) {
|
||||
if (!request || m_recording) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "recorder: cannot detach active recording"); return false; }
|
||||
if (m_compressor && m_compressorEnabled) {
|
||||
if (m_compressor && m_captureCompressorEnabled) {
|
||||
auto* replacement = audio_compressor_clone_config(m_compressor);
|
||||
if (!replacement) return false;
|
||||
request->compressor = m_compressor;
|
||||
@@ -192,24 +164,14 @@ bool AudioRecorder::takeRecording(struct attachment_send_req* request) {
|
||||
return true;
|
||||
}
|
||||
|
||||
float AudioRecorder::peakLevel() const {
|
||||
float v = m_peakLevel;
|
||||
m_peakLevel = 0.0f;
|
||||
return v;
|
||||
}
|
||||
float AudioRecorder::peakLevel() const { return m_peakLevel.exchange(0.0f); }
|
||||
|
||||
const int16_t* AudioRecorder::bufferData() const {
|
||||
if (m_compressor && m_compressorEnabled) return audio_compressor_output(m_compressor);
|
||||
return m_pcmBuffer.data();
|
||||
}
|
||||
|
||||
size_t AudioRecorder::bufferSize() const {
|
||||
if (m_compressor && m_compressorEnabled) return audio_compressor_output_size(m_compressor);
|
||||
return m_pcmBuffer.size();
|
||||
void AudioRecorder::configureCompressor(const audio_compressor_config_t& config) {
|
||||
m_compressorConfig = config; /* Применяется перед следующей записью, когда callback ещё не запущен. */
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "recorder: compressor configuration saved for next recording");
|
||||
}
|
||||
|
||||
void AudioRecorder::setCompressorEnabled(bool enabled) {
|
||||
m_compressorEnabled = enabled;
|
||||
if (m_compressor) audio_compressor_set_enabled(m_compressor, enabled);
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "compressor %s", enabled ? "enabled" : "disabled");
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "recorder: compressor=%d for next recording", enabled);
|
||||
}
|
||||
@@ -5,6 +5,10 @@
|
||||
#include <QTimer>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
#include "audio_device.h"
|
||||
extern "C" {
|
||||
#include "audio_compressor.h"
|
||||
}
|
||||
|
||||
struct ma_device;
|
||||
struct audio_compressor;
|
||||
@@ -20,8 +24,7 @@ public:
|
||||
void shutdown();
|
||||
bool isInitialized() const { return m_initialized; }
|
||||
|
||||
void setCaptureDeviceIndex(int index) { m_captureDeviceIndex = index; }
|
||||
int captureDeviceIndex() const { return m_captureDeviceIndex; }
|
||||
void setCaptureDevice(const QString& id) { m_captureId = id; m_customDevice = true; }
|
||||
|
||||
void startRecording();
|
||||
void stopRecording();
|
||||
@@ -29,13 +32,10 @@ public:
|
||||
|
||||
float peakLevel() const;
|
||||
|
||||
const int16_t* bufferData() const;
|
||||
size_t bufferSize() const;
|
||||
const std::vector<float>& waveformLevels() const { return m_waveformLevels; }
|
||||
int durationMs() const;
|
||||
bool takeRecording(struct attachment_send_req* request); /* Передать остановленную запись без копии PCM. */
|
||||
|
||||
struct audio_compressor* compressor() const { return m_compressor; }
|
||||
void configureCompressor(const audio_compressor_config_t& config);
|
||||
void setCompressorEnabled(bool enabled);
|
||||
bool isCompressorEnabled() const { return m_compressorEnabled; }
|
||||
|
||||
@@ -45,24 +45,26 @@ signals:
|
||||
void durationChanged(int ms);
|
||||
|
||||
private:
|
||||
ma_device* m_device = nullptr;
|
||||
AudioDevice m_capture;
|
||||
QTimer m_recoveryTimer;
|
||||
void recoverCapture();
|
||||
void captureCallback(const void* input, unsigned int frames);
|
||||
bool m_initialized = false;
|
||||
bool m_recording = false;
|
||||
bool m_deviceStarted = false;
|
||||
std::atomic<bool> m_recording{false};
|
||||
int m_sampleRate = 48000;
|
||||
int m_channels = 1;
|
||||
int m_captureDeviceIndex = -1;
|
||||
QString m_captureId;
|
||||
bool m_customDevice = false;
|
||||
|
||||
std::vector<int16_t> m_pcmBuffer;
|
||||
mutable float m_peakLevel = 0.0f;
|
||||
std::vector<float> m_recentRms = std::vector<float>(5, 0.0f);
|
||||
int m_rmsIdx = 0;
|
||||
std::vector<float> m_waveformLevels;
|
||||
mutable std::atomic<float> m_peakLevel{0.0f};
|
||||
std::atomic<uint64_t> m_recordedFrames{0};
|
||||
QTimer m_durationTimer;
|
||||
int m_elapsedMs = 0;
|
||||
|
||||
struct audio_compressor* m_compressor = nullptr;
|
||||
bool m_compressorEnabled = false;
|
||||
bool m_captureCompressorEnabled = false;
|
||||
audio_compressor_config_t m_compressorConfig{};
|
||||
|
||||
friend void audioCaptureCallback(ma_device* pDevice, void* pOutput, const void* pInput, unsigned int frameCount);
|
||||
};
|
||||
@@ -1,232 +1,164 @@
|
||||
#include "call_audio_engine.h"
|
||||
#include "sound_manager.h"
|
||||
#include "../transport/gui_bridge.h"
|
||||
|
||||
#include "miniaudio.h"
|
||||
|
||||
#include "../transport/audio_diagnostics.h"
|
||||
extern "C" {
|
||||
#include "call/call_audio.h"
|
||||
#include "../../lib/debug_config.h"
|
||||
#include "../../lib/u_async.h"
|
||||
#include "debug_config.h"
|
||||
}
|
||||
|
||||
#include <QTimer>
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
static CallAudioEngine* g_callAudioEngine = nullptr;
|
||||
|
||||
static void callAudioDataCallback(ma_device* dev, void* out, const void* in, ma_uint32 frames) {
|
||||
CallAudioEngine* e = (CallAudioEngine*)dev->pUserData;
|
||||
if (e) e->dataCallback(in, out, frames);
|
||||
}
|
||||
|
||||
CallAudioEngine* CallAudioEngine::instance() {
|
||||
if (!g_callAudioEngine) g_callAudioEngine = new CallAudioEngine();
|
||||
return g_callAudioEngine;
|
||||
static auto* engine = new CallAudioEngine();
|
||||
return engine;
|
||||
}
|
||||
|
||||
int CallAudioEngine::start(quint64 callId) {
|
||||
if (m_active) {
|
||||
if (m_callId == callId) return 0;
|
||||
stop();
|
||||
}
|
||||
m_callId = callId;
|
||||
|
||||
struct UTUN_INSTANCE* inst = gui_bridge_get_inst();
|
||||
if (!inst) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "CallAudio: no instance, cannot start");
|
||||
return -1;
|
||||
}
|
||||
if (call_audio_init(inst) != 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "CallAudio: call_audio_init failed");
|
||||
return -1;
|
||||
}
|
||||
if (call_audio_start(callId) != 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "CallAudio: call_audio_start failed call=%016llx", (unsigned long long)callId);
|
||||
return -1;
|
||||
}
|
||||
|
||||
m_captureBuf.assign(CALL_AUDIO_FRAME_SAMPLES, 0);
|
||||
m_capturePos = 0;
|
||||
m_muted = false;
|
||||
|
||||
if (initDevice() != 0) {
|
||||
call_audio_release(callId);
|
||||
return -1;
|
||||
}
|
||||
|
||||
m_active = true;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "CallAudio: started call=%016llx", (unsigned long long)callId);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int CallAudioEngine::initDevice() {
|
||||
ma_context* ctx = SoundManager::instance()->context();
|
||||
if (!ctx) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "CallAudio: no audio context");
|
||||
return -1;
|
||||
}
|
||||
|
||||
m_device = new ma_device;
|
||||
std::memset(m_device, 0, sizeof(*m_device));
|
||||
ma_device_config config = ma_device_config_init(ma_device_type_duplex);
|
||||
config.capture.format = ma_format_s16;
|
||||
config.capture.channels = 1;
|
||||
config.playback.format = ma_format_s16;
|
||||
config.playback.channels = 1;
|
||||
config.sampleRate = CALL_AUDIO_SAMPLE_RATE;
|
||||
config.periodSizeInFrames = 480;
|
||||
config.dataCallback = callAudioDataCallback;
|
||||
config.pUserData = this;
|
||||
|
||||
if (m_playbackDeviceIndex >= 0) {
|
||||
ma_device_info* pPlaybackDevices = nullptr;
|
||||
ma_uint32 playbackCount = 0;
|
||||
if (ma_context_get_devices(ctx, &pPlaybackDevices, &playbackCount, nullptr, nullptr) == MA_SUCCESS &&
|
||||
(ma_uint32)m_playbackDeviceIndex < playbackCount) {
|
||||
config.playback.pDeviceID = &pPlaybackDevices[m_playbackDeviceIndex].id;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "CallAudio: using playback device[%d]: %s",
|
||||
m_playbackDeviceIndex, pPlaybackDevices[m_playbackDeviceIndex].name);
|
||||
} else {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "CallAudio: playback device[%d] not found, using default",
|
||||
m_playbackDeviceIndex);
|
||||
}
|
||||
}
|
||||
|
||||
if (ma_device_init(ctx, &config, m_device) != MA_SUCCESS) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "CallAudio: device init failed, trying default");
|
||||
config.playback.pDeviceID = nullptr;
|
||||
if (ma_device_init(ctx, &config, m_device) != MA_SUCCESS) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "CallAudio: default device init also failed");
|
||||
delete m_device; m_device = nullptr;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "CallAudio: device backend=%s rate=%u capture='%s' rate=%u period=%u playback='%s' rate=%u period=%u",
|
||||
ma_get_backend_name(ctx->backend), m_device->sampleRate,
|
||||
m_device->capture.name, m_device->capture.internalSampleRate, m_device->capture.internalPeriodSizeInFrames,
|
||||
m_device->playback.name, m_device->playback.internalSampleRate, m_device->playback.internalPeriodSizeInFrames);
|
||||
m_capturePos = 0;
|
||||
m_dbg = {};
|
||||
if (ma_device_start(m_device) != MA_SUCCESS) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "CallAudio: device start failed");
|
||||
ma_device_uninit(m_device);
|
||||
delete m_device; m_device = nullptr;
|
||||
return -1;
|
||||
}
|
||||
m_deviceStarted = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void CallAudioEngine::setMuted(bool muted) {
|
||||
if (m_muted.load() == muted) return;
|
||||
m_muted = muted;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "CallAudio: mute %s", muted ? "on" : "off");
|
||||
}
|
||||
|
||||
void CallAudioEngine::setPlaybackDeviceIndex(int index) {
|
||||
if (index == m_playbackDeviceIndex) return;
|
||||
int prev = m_playbackDeviceIndex;
|
||||
m_playbackDeviceIndex = index;
|
||||
if (m_active) {
|
||||
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "CallAudio: switch playback device %d -> %d", prev, index);
|
||||
teardownDevice();
|
||||
if (initDevice() != 0)
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "CallAudio: switch playback device failed (kept index=%d)", index);
|
||||
}
|
||||
}
|
||||
|
||||
void CallAudioEngine::beginEnd(quint64 callId) {
|
||||
if (!m_active || m_callId != callId) return;
|
||||
call_audio_begin_end(callId); /* старт тона завершения */
|
||||
QTimer::singleShot(500, this, [this, callId]() {
|
||||
if (m_callId == callId) stop();
|
||||
CallAudioEngine::CallAudioEngine() {
|
||||
m_playbackId = SoundManager::instance()->playbackDevice();
|
||||
m_captureId = SoundManager::instance()->captureDevice();
|
||||
connect(SoundManager::instance(), &SoundManager::devicesChanged, this, [this]() {
|
||||
setPlaybackDevice(SoundManager::instance()->playbackDevice());
|
||||
setCaptureDevice(SoundManager::instance()->captureDevice());
|
||||
});
|
||||
m_recoveryTimer.setInterval(200);
|
||||
connect(&m_recoveryTimer, &QTimer::timeout, this, &CallAudioEngine::recoverDevices);
|
||||
connect(SoundManager::instance(), &SoundManager::contextAboutToReset, this, &CallAudioEngine::pauseDevices);
|
||||
connect(SoundManager::instance(), &SoundManager::contextRestored, this, [this]() {
|
||||
m_capture.retryNow(); m_playback.retryNow();
|
||||
if (m_active) recoverDevices();
|
||||
});
|
||||
}
|
||||
|
||||
void CallAudioEngine::stop() {
|
||||
if (!m_active && !m_device && !m_deviceStarted) return;
|
||||
m_active = false;
|
||||
int CallAudioEngine::start(quint64 callId) {
|
||||
if (m_active && m_callId == callId && !m_ending) return 0;
|
||||
stop();
|
||||
if (!gui_bridge_get_inst() || call_audio_start(callId) != 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "CallAudio: cannot start call=%016llx", (unsigned long long)callId);
|
||||
return -1;
|
||||
}
|
||||
++m_generation;
|
||||
m_callId = callId;
|
||||
m_captureBuf.assign(CALL_AUDIO_FRAME_SAMPLES, 0);
|
||||
m_capturePos = 0;
|
||||
m_muted = false;
|
||||
quint64 cid = m_callId.load();
|
||||
|
||||
teardownDevice();
|
||||
call_audio_release(cid);
|
||||
|
||||
m_callId = 0;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "CallAudio: stopped");
|
||||
m_ending = false;
|
||||
m_active = true;
|
||||
m_capture.retryNow(); m_playback.retryNow();
|
||||
recoverDevices();
|
||||
m_recoveryTimer.start();
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "CallAudio: session started call=%016llx", (unsigned long long)callId);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void CallAudioEngine::teardownDevice() {
|
||||
if (m_deviceStarted) {
|
||||
ma_device_stop(m_device);
|
||||
ma_device_uninit(m_device);
|
||||
m_deviceStarted = false;
|
||||
bool CallAudioEngine::openDevice(bool playback) {
|
||||
AudioDevice& device = playback ? m_playback : m_capture;
|
||||
ma_context* context = SoundManager::instance()->context();
|
||||
if (!context) return false;
|
||||
ma_device_config config = ma_device_config_init(playback ? ma_device_type_playback : ma_device_type_capture);
|
||||
config.capture.format = config.playback.format = ma_format_s16;
|
||||
config.capture.channels = config.playback.channels = 1;
|
||||
config.sampleRate = CALL_AUDIO_SAMPLE_RATE;
|
||||
ma_device_id id;
|
||||
bool selected = SoundManager::deviceId(playback ? m_playbackId : m_captureId, &id);
|
||||
if (selected) {
|
||||
if (playback) config.playback.pDeviceID = &id;
|
||||
else config.capture.pDeviceID = &id;
|
||||
}
|
||||
if (m_device) { delete m_device; m_device = nullptr; }
|
||||
auto callback = [this](const void* in, void* out, unsigned int frames) { dataCallback(in, out, frames); };
|
||||
const char* role = playback ? "call-playback" : "call-capture";
|
||||
auto attempt = [&]() {
|
||||
if (!playback) m_capturePos = 0;
|
||||
bool ready = device.init(context, config, role, m_callId.load(), callback) && device.start();
|
||||
if (!ready) call_audio_reset_io(m_callId.load(), playback);
|
||||
return ready;
|
||||
};
|
||||
if (attempt()) return true;
|
||||
if (!selected) return false;
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "CallAudio: %s selected device unavailable, trying default", role);
|
||||
config.capture.pDeviceID = config.playback.pDeviceID = nullptr;
|
||||
return attempt();
|
||||
}
|
||||
|
||||
void CallAudioEngine::recoverDevices() {
|
||||
if (!m_active) return;
|
||||
if (m_playback.needsRecovery()) {
|
||||
m_playback.close();
|
||||
call_audio_reset_io(m_callId.load(), 1);
|
||||
m_playback.retryLater();
|
||||
}
|
||||
if (!m_ending && m_capture.needsRecovery()) {
|
||||
m_capture.close();
|
||||
m_capturePos = 0;
|
||||
call_audio_reset_io(m_callId.load(), 0);
|
||||
m_capture.retryLater();
|
||||
}
|
||||
if (m_playback.retryReady()) openDevice(true);
|
||||
if (!m_ending && m_capture.retryReady()) openDevice(false);
|
||||
}
|
||||
|
||||
void CallAudioEngine::pauseDevices() {
|
||||
m_capture.close(); m_playback.close();
|
||||
m_capturePos = 0;
|
||||
if (m_active) call_audio_reset_io(m_callId.load(), 1);
|
||||
}
|
||||
|
||||
void CallAudioEngine::setMuted(bool muted) {
|
||||
if (m_muted.exchange(muted) == muted) return;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "CallAudio: mute=%d", muted);
|
||||
}
|
||||
|
||||
void CallAudioEngine::setPlaybackDevice(const QString& id) {
|
||||
if (id == m_playbackId) return;
|
||||
m_playbackId = id;
|
||||
m_playback.close(); m_playback.retryNow();
|
||||
if (m_active) { call_audio_reset_io(m_callId.load(), 1); recoverDevices(); }
|
||||
}
|
||||
|
||||
void CallAudioEngine::setCaptureDevice(const QString& id) {
|
||||
if (id == m_captureId) return;
|
||||
m_captureId = id;
|
||||
m_capture.close(); m_capture.retryNow();
|
||||
m_capturePos = 0;
|
||||
if (m_active) { call_audio_reset_io(m_callId.load(), 0); recoverDevices(); }
|
||||
}
|
||||
|
||||
void CallAudioEngine::beginEnd(quint64 callId) {
|
||||
if (!m_active || m_callId != callId || m_ending) return;
|
||||
m_ending = true;
|
||||
m_capture.close();
|
||||
call_audio_begin_end(callId);
|
||||
uint64_t generation = m_generation;
|
||||
QTimer::singleShot(500, this, [this, generation]() { if (m_generation == generation) stop(); });
|
||||
}
|
||||
|
||||
void CallAudioEngine::stop() {
|
||||
++m_generation;
|
||||
m_recoveryTimer.stop();
|
||||
m_active = false;
|
||||
pauseDevices();
|
||||
uint64_t id = m_callId.exchange(0);
|
||||
if (id) call_audio_release(id);
|
||||
m_muted = false;
|
||||
m_ending = false;
|
||||
if (id) DEBUG_INFO(DEBUG_CATEGORY_CALL, "CallAudio: session stopped call=%016llx", (unsigned long long)id);
|
||||
}
|
||||
|
||||
void CallAudioEngine::dataCallback(const void* in, void* out, unsigned int frames) {
|
||||
const int16_t* in16 = (const int16_t*)in;
|
||||
int16_t* out16 = (int16_t*)out;
|
||||
uint64_t startTb = get_time_tb();
|
||||
uint64_t idleTb = m_dbg.lastEndTb ? startTb - m_dbg.lastEndTb : 0;
|
||||
if (!m_dbg.lastReportTb) m_dbg.lastReportTb = startTb;
|
||||
|
||||
/* диагностика (DEBUG_CATEGORY_DEBUG): фактические параметры miniaudio-коллбэка */
|
||||
if (m_dbg.frames != frames) {
|
||||
m_dbg.frames = frames;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "CallAudio: callback frames=%u in=%s out=%s",
|
||||
frames, in16 ? "yes" : "no", out16 ? "yes" : "no");
|
||||
}
|
||||
|
||||
/* захват: копим 960 samples → encode+send (сервисный слой). mute → тишина. */
|
||||
uint64_t captureStartTb = get_time_tb();
|
||||
if (in16) {
|
||||
if (in) {
|
||||
const auto* pcm = static_cast<const int16_t*>(in);
|
||||
bool muted = m_muted.load();
|
||||
for (unsigned int i = 0; i < frames; i++) {
|
||||
m_captureBuf[m_capturePos++] = muted ? 0 : in16[i];
|
||||
if (m_capturePos >= (int)m_captureBuf.size()) {
|
||||
call_audio_feed_pcm(m_callId.load(), m_captureBuf.data(), (int)m_captureBuf.size());
|
||||
for (unsigned int i = 0; i < frames; ++i) {
|
||||
m_captureBuf[m_capturePos++] = muted ? 0 : pcm[i];
|
||||
if (m_capturePos == CALL_AUDIO_FRAME_SAMPLES) {
|
||||
int result = call_audio_feed_pcm(m_callId.load(), m_captureBuf.data(), CALL_AUDIO_FRAME_SAMPLES);
|
||||
if (result != 0) audio_diag_event(m_capture.device(), AUDIO_DIAG_ERROR, AUDIO_DIAG_FEED, 1, result);
|
||||
m_capturePos = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
uint64_t captureEndTb = get_time_tb();
|
||||
|
||||
/* воспроизведение: финальный PCM из сервисного слоя (jitter+stretch+тоны) */
|
||||
int n = call_audio_pull_pcm(m_callId.load(), out16, (int)frames);
|
||||
if (n < (int)frames)
|
||||
std::memset(out16 + n, 0, (size_t)(frames - n) * sizeof(int16_t));
|
||||
uint64_t endTb = get_time_tb();
|
||||
uint64_t captureTb = captureEndTb - captureStartTb;
|
||||
uint64_t playbackTb = endTb - captureEndTb;
|
||||
m_dbg.callbacks++;
|
||||
m_dbg.captureFrames += in16 ? frames : 0;
|
||||
m_dbg.playbackFrames += out16 ? frames : 0;
|
||||
m_dbg.maxIdleTb = std::max(m_dbg.maxIdleTb, idleTb);
|
||||
m_dbg.maxCaptureTb = std::max(m_dbg.maxCaptureTb, captureTb);
|
||||
m_dbg.maxPlaybackTb = std::max(m_dbg.maxPlaybackTb, playbackTb);
|
||||
if (idleTb > 500 || endTb - startTb > 200) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "CallAudio: stall call=%016llx idle=%.1fms work=%.1fms capture=%.1fms playback=%.1fms frames=%u",
|
||||
(unsigned long long)m_callId.load(), idleTb / 10.0, (endTb - startTb) / 10.0,
|
||||
captureTb / 10.0, playbackTb / 10.0, frames);
|
||||
if (out) {
|
||||
auto* pcm = static_cast<int16_t*>(out);
|
||||
int n = call_audio_pull_pcm(m_callId.load(), pcm, (int)frames);
|
||||
if (n < (int)frames) std::memset(pcm + n, 0, (frames - n) * sizeof(*pcm));
|
||||
}
|
||||
uint64_t elapsedTb = endTb - m_dbg.lastReportTb;
|
||||
if (elapsedTb >= 10000) {
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG,
|
||||
"CallAudio: interval=%.1fms cb=%.1f/s capture=%.0f/s playback=%.0f/s max_idle=%.1fms max_capture=%.1fms max_playback=%.1fms",
|
||||
elapsedTb / 10.0, m_dbg.callbacks * 10000.0 / elapsedTb,
|
||||
m_dbg.captureFrames * 10000.0 / elapsedTb, m_dbg.playbackFrames * 10000.0 / elapsedTb,
|
||||
m_dbg.maxIdleTb / 10.0, m_dbg.maxCaptureTb / 10.0, m_dbg.maxPlaybackTb / 10.0);
|
||||
m_dbg.callbacks = 0;
|
||||
m_dbg.captureFrames = 0;
|
||||
m_dbg.playbackFrames = 0;
|
||||
m_dbg.maxIdleTb = 0;
|
||||
m_dbg.maxCaptureTb = 0;
|
||||
m_dbg.maxPlaybackTb = 0;
|
||||
m_dbg.lastReportTb = endTb;
|
||||
}
|
||||
m_dbg.lastEndTb = get_time_tb();
|
||||
}
|
||||
@@ -4,6 +4,8 @@
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include <QTimer>
|
||||
#include "audio_device.h"
|
||||
|
||||
struct ma_device;
|
||||
|
||||
@@ -12,8 +14,7 @@ struct ma_device;
|
||||
* слое src/call/call_audio.c; сюда приходит/отсюда уходит финальный PCM.
|
||||
*
|
||||
* Потоки:
|
||||
* - аудио-поток (miniaudio callback): capture → call_audio_feed_pcm,
|
||||
* call_audio_pull_pcm → playback;
|
||||
* - независимые callbacks capture → call_audio_feed_pcm и call_audio_pull_pcm → playback;
|
||||
* - GUI-поток: start()/beginEnd()/stop() по ACCEPTED/ENDED. */
|
||||
class CallAudioEngine : public QObject {
|
||||
Q_OBJECT
|
||||
@@ -36,40 +37,32 @@ public:
|
||||
void setMuted(bool muted);
|
||||
bool isMuted() const { return m_muted.load(); }
|
||||
|
||||
/* Устройство вывода звонка (-1 = default). Смена на лету пересоздаёт ma_device. */
|
||||
void setPlaybackDeviceIndex(int index);
|
||||
int playbackDeviceIndex() const { return m_playbackDeviceIndex; }
|
||||
/* Backend ID (пустой = default). Смена пересоздаёт только соответствующее устройство. */
|
||||
void setPlaybackDevice(const QString& id);
|
||||
QString playbackDevice() const { return m_playbackId; }
|
||||
void setCaptureDevice(const QString& id);
|
||||
|
||||
/* аудио-поток: miniaudio data callback (захват + воспроизведение) */
|
||||
void dataCallback(const void* in, void* out, unsigned int frames);
|
||||
|
||||
private:
|
||||
CallAudioEngine() = default;
|
||||
void teardownDevice();
|
||||
int initDevice();
|
||||
CallAudioEngine();
|
||||
bool openDevice(bool playback);
|
||||
void recoverDevices();
|
||||
void pauseDevices();
|
||||
|
||||
std::atomic<bool> m_active{false};
|
||||
std::atomic<uint64_t> m_callId{0};
|
||||
std::atomic<bool> m_muted{false};
|
||||
|
||||
int m_playbackDeviceIndex = -1; /* только GUI-поток */
|
||||
QString m_playbackId; /* только GUI-поток */
|
||||
|
||||
ma_device* m_device = nullptr;
|
||||
bool m_deviceStarted = false;
|
||||
QString m_captureId;
|
||||
AudioDevice m_capture, m_playback;
|
||||
QTimer m_recoveryTimer;
|
||||
uint64_t m_generation = 0;
|
||||
bool m_ending = false;
|
||||
|
||||
std::vector<int16_t> m_captureBuf; /* 960 samples, аудио-поток */
|
||||
int m_capturePos = 0;
|
||||
|
||||
/* Только аудио-поток; сброс до ma_device_start. Время в 0.1мс. */
|
||||
struct {
|
||||
unsigned int frames = 0;
|
||||
uint32_t callbacks = 0;
|
||||
uint64_t captureFrames = 0;
|
||||
uint64_t playbackFrames = 0;
|
||||
uint64_t lastReportTb = 0;
|
||||
uint64_t lastEndTb = 0;
|
||||
uint64_t maxIdleTb = 0;
|
||||
uint64_t maxCaptureTb = 0;
|
||||
uint64_t maxPlaybackTb = 0;
|
||||
} m_dbg;
|
||||
};
|
||||
@@ -279,16 +279,16 @@ void CallWindow::setState(State s) {
|
||||
void CallWindow::populateDeviceCombo() {
|
||||
m_deviceCombo->blockSignals(true);
|
||||
m_deviceCombo->clear();
|
||||
m_deviceCombo->addItem(QStringLiteral("Default (auto)"), -1);
|
||||
m_deviceCombo->addItem(QStringLiteral("Default (auto)"), QString());
|
||||
const auto devs = SoundManager::enumeratePlaybackDevices();
|
||||
for (int i = 0; i < devs.size(); i++) {
|
||||
QString label = devs[i].name;
|
||||
if (devs[i].isDefault) label += QStringLiteral(" (default)");
|
||||
m_deviceCombo->addItem(label, i);
|
||||
m_deviceCombo->addItem(label, devs[i].id);
|
||||
}
|
||||
int cur = CallAudioEngine::instance()->playbackDeviceIndex();
|
||||
QString cur = CallAudioEngine::instance()->playbackDevice();
|
||||
for (int i = 0; i < m_deviceCombo->count(); i++) {
|
||||
if (m_deviceCombo->itemData(i).toInt() == cur) { m_deviceCombo->setCurrentIndex(i); break; }
|
||||
if (m_deviceCombo->itemData(i).toString() == cur) { m_deviceCombo->setCurrentIndex(i); break; }
|
||||
}
|
||||
m_deviceCombo->blockSignals(false);
|
||||
}
|
||||
@@ -332,8 +332,8 @@ void CallWindow::onMuteToggled(bool checked) {
|
||||
}
|
||||
|
||||
void CallWindow::onDeviceChanged(int index) {
|
||||
int devIdx = m_deviceCombo->itemData(index).toInt();
|
||||
CallAudioEngine::instance()->setPlaybackDeviceIndex(devIdx);
|
||||
QString id = m_deviceCombo->itemData(index).toString();
|
||||
CallAudioEngine::instance()->setPlaybackDevice(id);
|
||||
}
|
||||
|
||||
void CallWindow::closeEvent(QCloseEvent* event) {
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "voicemessageencoder.h"
|
||||
#include "callwindow.h"
|
||||
#include "call_audio_engine.h"
|
||||
#include "radio_audio_engine.h"
|
||||
#include "ptt_hotkey_manager.h"
|
||||
|
||||
extern "C" {
|
||||
@@ -177,7 +178,8 @@ MainWindow::~MainWindow() {
|
||||
gui_bridge_set_channel_peers_online_cb(nullptr);
|
||||
gui_bridge_set_db_ready_cb(nullptr);
|
||||
gui_bridge_set_attachment_downloaded_cb(nullptr);
|
||||
QCoreApplication::processEvents(QEventLoop::AllEvents);
|
||||
CallAudioEngine::instance()->stop();
|
||||
RadioAudioEngine::instance()->stop();
|
||||
if (m_recorder) { m_recorder->shutdown(); m_recorder = nullptr; }
|
||||
SoundManager::instance()->shutdown();
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "MainWindow: ~MainWindow — saving channel state");
|
||||
@@ -335,6 +337,7 @@ void MainWindow::setupSoundFromConfig() {
|
||||
if (!connectFile.isEmpty()) sm->loadSound("connect", resolvePath(connectFile));
|
||||
if (!disconnectFile.isEmpty()) sm->loadSound("disconnect", resolvePath(disconnectFile));
|
||||
if (m_db && m_db->isOpen()) {
|
||||
sm->setDevices(m_db->getUiState("audio_output_id"), m_db->getUiState("audio_input_id"));
|
||||
sm->setEnabled(m_db->getUiStateInt("sound_enabled", 1));
|
||||
sm->setVolume(m_db->getUiStateInt("sound_volume", 80) / 100.0f);
|
||||
QString se[] = {"msg_incoming", "msg_outgoing", "connect", "disconnect",
|
||||
@@ -349,12 +352,11 @@ void MainWindow::setupSoundFromConfig() {
|
||||
if (m_recorder) {
|
||||
m_recorder->init();
|
||||
if (m_db && m_db->isOpen()) {
|
||||
m_recorder->setCaptureDeviceIndex(m_db->getUiStateInt("audio_input_device", -1));
|
||||
VoiceEncoder::setPreset(m_db->getUiStateInt("opus_codec_preset", 1));
|
||||
|
||||
int compEnabled = m_db->getUiStateInt("compressor_enabled", 0);
|
||||
m_recorder->setCompressorEnabled(compEnabled != 0);
|
||||
if (m_recorder->compressor()) {
|
||||
{
|
||||
audio_compressor_config_t compCfg = {0};
|
||||
compCfg.sample_rate = 48000;
|
||||
compCfg.channels = 1;
|
||||
@@ -363,11 +365,11 @@ void MainWindow::setupSoundFromConfig() {
|
||||
compCfg.lookahead_ms = 100;
|
||||
compCfg.rise_rate_per_sec = (float)m_db->getUiStateInt("compressor_rise_rate", 10);
|
||||
compCfg.target_level = 1.0f;
|
||||
audio_compressor_configure(m_recorder->compressor(), &compCfg);
|
||||
m_recorder->configureCompressor(compCfg);
|
||||
}
|
||||
}
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "AudioRecorder initialized (captureDevice=%d, opusPreset=%d, compressor=%d)",
|
||||
m_recorder->captureDeviceIndex(), VoiceEncoder::preset(),
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "AudioRecorder initialized (opusPreset=%d, compressor=%d)",
|
||||
VoiceEncoder::preset(),
|
||||
m_recorder->isCompressorEnabled() ? 1 : 0);
|
||||
}
|
||||
}
|
||||
@@ -598,8 +600,7 @@ void MainWindow::onCallAccepted(quint64 callId) {
|
||||
SoundManager::instance()->stopLoop();
|
||||
CallWindow* w = m_activeCalls.value(callId, nullptr);
|
||||
if (w) w->setState(CallWindow::Active);
|
||||
if (m_db)
|
||||
CallAudioEngine::instance()->setPlaybackDeviceIndex(m_db->getUiStateInt("audio_output_device", -1));
|
||||
if (!s_mainWindow) return;
|
||||
CallAudioEngine::instance()->start(callId);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "radio_audio_engine.h"
|
||||
#include "sound_manager.h"
|
||||
#include "../transport/audio_diagnostics.h"
|
||||
#include "../transport/gui_bridge.h"
|
||||
|
||||
#include "miniaudio.h"
|
||||
@@ -8,162 +9,140 @@ extern "C" {
|
||||
#include "radio/radio_audio.h"
|
||||
#include "chat/chat_setting.h"
|
||||
#include "../../lib/debug_config.h"
|
||||
#include "../../lib/u_async.h"
|
||||
}
|
||||
|
||||
#include <cstring>
|
||||
|
||||
static RadioAudioEngine* g_radioAudioEngine = nullptr;
|
||||
|
||||
static void radioAudioCaptureCallback(ma_device* dev, void* out, const void* in, ma_uint32 frames) {
|
||||
(void)out;
|
||||
RadioAudioEngine* e = (RadioAudioEngine*)dev->pUserData;
|
||||
if (e) e->captureCallback(in, frames);
|
||||
}
|
||||
|
||||
static void radioAudioPlaybackCallback(ma_device* dev, void* out, const void* in, ma_uint32 frames) {
|
||||
(void)in;
|
||||
RadioAudioEngine* e = (RadioAudioEngine*)dev->pUserData;
|
||||
if (e) e->playbackCallback(out, frames);
|
||||
}
|
||||
#include <algorithm>
|
||||
|
||||
RadioAudioEngine* RadioAudioEngine::instance() {
|
||||
if (!g_radioAudioEngine) g_radioAudioEngine = new RadioAudioEngine();
|
||||
return g_radioAudioEngine;
|
||||
static auto* engine = new RadioAudioEngine();
|
||||
return engine;
|
||||
}
|
||||
|
||||
RadioAudioEngine::RadioAudioEngine() {
|
||||
m_captureId = SoundManager::instance()->captureDevice();
|
||||
m_playbackId = SoundManager::instance()->playbackDevice();
|
||||
connect(SoundManager::instance(), &SoundManager::devicesChanged, this, [this]() {
|
||||
auto* manager = SoundManager::instance();
|
||||
if (m_captureId != manager->captureDevice()) {
|
||||
m_captureId = manager->captureDevice();
|
||||
m_capture.close(); if (m_active) radio_audio_capture_stop(); m_capture.retryNow();
|
||||
}
|
||||
if (m_playbackId != manager->playbackDevice()) {
|
||||
m_playbackId = manager->playbackDevice();
|
||||
m_playback.close(); if (m_active) radio_audio_reset_playback(m_groupId.load()); m_playback.retryNow();
|
||||
}
|
||||
recoverDevices();
|
||||
});
|
||||
m_recoveryTimer.setInterval(200);
|
||||
connect(&m_recoveryTimer, &QTimer::timeout, this, &RadioAudioEngine::recoverDevices);
|
||||
connect(SoundManager::instance(), &SoundManager::contextAboutToReset, this, &RadioAudioEngine::pauseDevices);
|
||||
connect(SoundManager::instance(), &SoundManager::contextRestored, this, [this]() {
|
||||
m_capture.retryNow(); m_playback.retryNow();
|
||||
recoverDevices();
|
||||
});
|
||||
}
|
||||
|
||||
int RadioAudioEngine::start(quint64 groupId) {
|
||||
if (m_active) {
|
||||
if (m_groupId == groupId) return 0;
|
||||
stop();
|
||||
if (m_active && m_groupId == groupId) return 0;
|
||||
stop();
|
||||
auto* inst = gui_bridge_get_inst();
|
||||
if (!inst || radio_audio_start(groupId, 0) != 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "RadioAudio: cannot start group=%016llx", (unsigned long long)groupId);
|
||||
return -1;
|
||||
}
|
||||
m_groupId = groupId;
|
||||
|
||||
struct UTUN_INSTANCE* inst = gui_bridge_get_inst();
|
||||
if (!inst) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "RadioAudio: no instance, cannot start");
|
||||
return -1;
|
||||
}
|
||||
if (radio_audio_init(inst) != 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "RadioAudio: radio_audio_init failed");
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* желаемые каналы захвата из настройки; initDevice() при недоступности откатывает на моно */
|
||||
m_captureChannels = chat_setting_get_int(inst, "radio_capture_stereo", 0) ? 2 : 1;
|
||||
|
||||
if (initDevice() != 0) {
|
||||
radio_audio_stop();
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (radio_audio_start(groupId, m_captureChannels) != 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "RadioAudio: radio_audio_start failed grp=%016llx", (unsigned long long)groupId);
|
||||
teardownDevice();
|
||||
radio_audio_stop();
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* VAD авто-PTT: в этом режиме захват фидится постоянно, решение «говорить» — в сервисном слое. */
|
||||
m_vadMode = radio_audio_vad_mode() ? true : false;
|
||||
if (m_vadMode)
|
||||
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "RadioAudio: VAD auto-PTT enabled grp=%016llx", (unsigned long long)groupId);
|
||||
|
||||
m_captureBuf.assign((size_t)RADIO_AUDIO_FRAME_SAMPLES * m_captureChannels, 0);
|
||||
m_capturePos = 0;
|
||||
|
||||
m_wantStereo = chat_setting_get_int(inst, "radio_capture_stereo", 0) != 0;
|
||||
m_active = true;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "RadioAudio: started grp=%016llx captureCh=%d", (unsigned long long)groupId, m_captureChannels);
|
||||
m_capture.retryNow(); m_playback.retryNow();
|
||||
recoverDevices();
|
||||
m_recoveryTimer.start();
|
||||
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "RadioAudio: listening group=%016llx", (unsigned long long)groupId);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int RadioAudioEngine::initDevice() {
|
||||
ma_context* ctx = SoundManager::instance()->context();
|
||||
if (!ctx) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "RadioAudio: no audio context");
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* ── capture-устройство (стерео → откат на моно → дефолтное устройство) ── */
|
||||
const int wantCh = m_captureChannels;
|
||||
int capCh = 0;
|
||||
for (int useDefault = 0; useDefault < 2 && capCh == 0; useDefault++) {
|
||||
for (int ch = wantCh; ch >= 1; ch--) {
|
||||
ma_device* dev = new ma_device;
|
||||
std::memset(dev, 0, sizeof(*dev));
|
||||
ma_device_config config = ma_device_config_init(ma_device_type_capture);
|
||||
config.capture.format = ma_format_s16;
|
||||
config.capture.channels = ch;
|
||||
config.sampleRate = RADIO_AUDIO_SAMPLE_RATE;
|
||||
config.periodSizeInFrames = 480;
|
||||
config.dataCallback = radioAudioCaptureCallback;
|
||||
config.pUserData = this;
|
||||
if (useDefault) config.capture.pDeviceID = nullptr;
|
||||
|
||||
if (ma_device_init(ctx, &config, dev) != MA_SUCCESS) { delete dev; continue; }
|
||||
if (ma_device_start(dev) != MA_SUCCESS) { ma_device_uninit(dev); delete dev; continue; }
|
||||
m_captureDevice = dev;
|
||||
m_captureStarted = true;
|
||||
capCh = ch;
|
||||
if (ch != wantCh)
|
||||
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "RadioAudio: stereo capture unavailable — fallback to mono");
|
||||
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "RadioAudio: capture started ch=%d cap='%s'",
|
||||
ch, dev->capture.name ? dev->capture.name : "?");
|
||||
break;
|
||||
void RadioAudioEngine::openCapture() {
|
||||
auto* context = SoundManager::instance()->context();
|
||||
if (!context) return;
|
||||
for (int channels = m_wantStereo ? 2 : 1; channels >= 1; --channels) {
|
||||
ma_device_config config = ma_device_config_init(ma_device_type_capture);
|
||||
config.capture.format = ma_format_s16;
|
||||
config.capture.channels = channels;
|
||||
config.sampleRate = RADIO_AUDIO_SAMPLE_RATE;
|
||||
auto callback = [this](const void* in, void*, unsigned int frames) { captureCallback(in, frames); };
|
||||
ma_device_id id;
|
||||
bool selected = SoundManager::deviceId(m_captureId, &id);
|
||||
for (int attempt = 0; attempt < (selected ? 2 : 1); ++attempt) {
|
||||
config.capture.pDeviceID = selected && attempt == 0 ? &id : nullptr;
|
||||
if (attempt) DEBUG_WARN(DEBUG_CATEGORY_RADIO, "RadioAudio: selected input unavailable, trying default ch=%d", channels);
|
||||
if (!m_capture.init(context, config, "radio-capture", m_groupId.load(), callback)) continue;
|
||||
m_captureChannels = channels;
|
||||
m_captureBuf.assign(RADIO_AUDIO_FRAME_SAMPLES * channels, 0);
|
||||
m_capturePos = 0;
|
||||
auto* inst = gui_bridge_get_inst();
|
||||
int vad = chat_setting_get_int(inst, "radio_vad_enabled", 0);
|
||||
if (radio_audio_capture_start(m_groupId.load(), channels, vad) != 0) {
|
||||
m_capture.close(); m_capture.retryLater(); return;
|
||||
}
|
||||
m_vadMode = radio_audio_vad_mode() != 0;
|
||||
if (!m_capture.start()) { radio_audio_capture_stop(); continue; }
|
||||
if (m_talking) radio_audio_talk_begin(m_groupId.load());
|
||||
if (channels == 1 && m_wantStereo) DEBUG_WARN(DEBUG_CATEGORY_RADIO, "RadioAudio: stereo input unavailable, using mono");
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (capCh == 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "RadioAudio: capture device init failed");
|
||||
teardownDevice();
|
||||
return -1;
|
||||
}
|
||||
|
||||
void RadioAudioEngine::openPlayback() {
|
||||
auto* context = SoundManager::instance()->context();
|
||||
if (!context) return;
|
||||
ma_device_config config = ma_device_config_init(ma_device_type_playback);
|
||||
config.playback.format = ma_format_s16;
|
||||
config.playback.channels = 2;
|
||||
config.sampleRate = RADIO_AUDIO_SAMPLE_RATE;
|
||||
auto callback = [this](const void*, void* out, unsigned int frames) { playbackCallback(out, frames); };
|
||||
ma_device_id id;
|
||||
bool selected = SoundManager::deviceId(m_playbackId, &id);
|
||||
if (selected) config.playback.pDeviceID = &id;
|
||||
if (m_playback.init(context, config, "radio-playback", m_groupId.load(), callback) && m_playback.start()) return;
|
||||
if (!selected) return;
|
||||
DEBUG_WARN(DEBUG_CATEGORY_RADIO, "RadioAudio: selected output unavailable, trying default");
|
||||
config.playback.pDeviceID = nullptr;
|
||||
if (m_playback.init(context, config, "radio-playback", m_groupId.load(), callback)) m_playback.start();
|
||||
}
|
||||
|
||||
void RadioAudioEngine::recoverDevices() {
|
||||
if (!m_active) return;
|
||||
if (m_capture.needsRecovery()) {
|
||||
m_capture.close(); radio_audio_capture_stop();
|
||||
m_capturePos = 0; m_capture.retryLater();
|
||||
}
|
||||
m_captureChannels = capCh;
|
||||
|
||||
/* ── playback-устройство (всегда стерео) ── */
|
||||
m_playbackDevice = new ma_device;
|
||||
std::memset(m_playbackDevice, 0, sizeof(*m_playbackDevice));
|
||||
{
|
||||
ma_device_config config = ma_device_config_init(ma_device_type_playback);
|
||||
config.playback.format = ma_format_s16;
|
||||
config.playback.channels = 2;
|
||||
config.sampleRate = RADIO_AUDIO_SAMPLE_RATE;
|
||||
config.periodSizeInFrames = 480;
|
||||
config.dataCallback = radioAudioPlaybackCallback;
|
||||
config.pUserData = this;
|
||||
|
||||
if (ma_device_init(ctx, &config, m_playbackDevice) != MA_SUCCESS) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "RadioAudio: playback device init failed");
|
||||
teardownDevice();
|
||||
return -1;
|
||||
}
|
||||
if (ma_device_start(m_playbackDevice) != MA_SUCCESS) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "RadioAudio: playback device start failed");
|
||||
teardownDevice();
|
||||
return -1;
|
||||
}
|
||||
m_playbackStarted = true;
|
||||
if (m_playback.needsRecovery()) {
|
||||
m_playback.close(); radio_audio_reset_playback(m_groupId.load()); m_playback.retryLater();
|
||||
}
|
||||
if (m_playback.retryReady()) openPlayback();
|
||||
if (m_capture.retryReady()) openCapture();
|
||||
}
|
||||
|
||||
void RadioAudioEngine::pauseDevices() {
|
||||
m_capture.close(); m_playback.close();
|
||||
m_capturePos = 0;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "RadioAudio: playback started ch=2 pb='%s'",
|
||||
m_playbackDevice->playback.name ? m_playbackDevice->playback.name : "?");
|
||||
return 0;
|
||||
if (m_active) {
|
||||
radio_audio_capture_stop();
|
||||
radio_audio_reset_playback(m_groupId.load());
|
||||
}
|
||||
}
|
||||
|
||||
void RadioAudioEngine::stop() {
|
||||
if (!m_active && !m_captureDevice && !m_playbackDevice && !m_captureStarted && !m_playbackStarted) return;
|
||||
quint64 gid = m_groupId.load();
|
||||
m_recoveryTimer.stop();
|
||||
m_capture.close(); m_playback.close();
|
||||
uint64_t group = m_groupId.exchange(0);
|
||||
bool talking = m_talking.exchange(false);
|
||||
m_buttonTalking = m_hotkeyTalking = false;
|
||||
bool wasTalking = m_talking.exchange(false);
|
||||
if (wasTalking) radio_audio_talk_end(gid); /* честный FIN: пиры дренируют звук, а не ждут таймаут */
|
||||
m_active = false;
|
||||
|
||||
teardownDevice();
|
||||
radio_audio_stop();
|
||||
|
||||
m_groupId = 0;
|
||||
if (wasTalking) emit talkingChanged(false);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "RadioAudio: stopped");
|
||||
if (group) radio_audio_stop();
|
||||
if (talking) emit talkingChanged(false);
|
||||
if (group) DEBUG_INFO(DEBUG_CATEGORY_RADIO, "RadioAudio: stopped group=%016llx", (unsigned long long)group);
|
||||
}
|
||||
|
||||
void RadioAudioEngine::talkBegin() {
|
||||
@@ -192,21 +171,6 @@ void RadioAudioEngine::updateTalking() {
|
||||
emit talkingChanged(talking);
|
||||
}
|
||||
|
||||
void RadioAudioEngine::teardownDevice() {
|
||||
if (m_captureStarted && m_captureDevice) {
|
||||
ma_device_stop(m_captureDevice);
|
||||
ma_device_uninit(m_captureDevice);
|
||||
m_captureStarted = false;
|
||||
}
|
||||
if (m_captureDevice) { delete m_captureDevice; m_captureDevice = nullptr; }
|
||||
if (m_playbackStarted && m_playbackDevice) {
|
||||
ma_device_stop(m_playbackDevice);
|
||||
ma_device_uninit(m_playbackDevice);
|
||||
m_playbackStarted = false;
|
||||
}
|
||||
if (m_playbackDevice) { delete m_playbackDevice; m_playbackDevice = nullptr; }
|
||||
}
|
||||
|
||||
void RadioAudioEngine::captureCallback(const void* in, unsigned int frames) {
|
||||
const int16_t* in16 = (const int16_t*)in;
|
||||
if (!in16) return;
|
||||
@@ -219,7 +183,8 @@ void RadioAudioEngine::captureCallback(const void* in, unsigned int frames) {
|
||||
for (int i = 0; i < totalIn; i++) {
|
||||
m_captureBuf[m_capturePos++] = in16[i];
|
||||
if (m_capturePos >= (int)m_captureBuf.size()) {
|
||||
radio_audio_feed_pcm(m_groupId.load(), m_captureBuf.data(), (int)m_captureBuf.size());
|
||||
int result = radio_audio_feed_pcm(m_groupId.load(), m_captureBuf.data(), (int)m_captureBuf.size());
|
||||
if (result != 0) audio_diag_event(m_capture.device(), AUDIO_DIAG_ERROR, AUDIO_DIAG_FEED, 1, result);
|
||||
m_capturePos = 0;
|
||||
}
|
||||
}
|
||||
@@ -228,36 +193,12 @@ void RadioAudioEngine::captureCallback(const void* in, unsigned int frames) {
|
||||
void RadioAudioEngine::playbackCallback(void* out, unsigned int frames) {
|
||||
int16_t* out16 = (int16_t*)out;
|
||||
|
||||
/* диагностика (DEBUG_CATEGORY_DEBUG): cadence playback-коллбэка */
|
||||
uint64_t now = get_time_tb();
|
||||
if (m_dbgFrames == 0) {
|
||||
m_dbgFrames = frames;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "RadioAudio: playback frames=%u", frames);
|
||||
}
|
||||
m_dbgCbCount++;
|
||||
if (m_dbgPrevCbTb != 0) {
|
||||
uint64_t gap = now - m_dbgPrevCbTb;
|
||||
if (gap > m_dbgGapMax) m_dbgGapMax = gap;
|
||||
}
|
||||
m_dbgPrevCbTb = now;
|
||||
if (m_dbgLastTb == 0) {
|
||||
m_dbgLastTb = now;
|
||||
} else if (now - m_dbgLastTb >= 10000) {
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "RadioAudio: pb cb=%u/s frames=%u gapMax=%.1fms bodyMax=%.1fms",
|
||||
m_dbgCbCount, frames, (double)m_dbgGapMax / 10.0, (double)m_dbgBodyMax / 10.0);
|
||||
m_dbgCbCount = 0;
|
||||
m_dbgLastTb = now;
|
||||
m_dbgGapMax = 0;
|
||||
m_dbgBodyMax = 0;
|
||||
}
|
||||
|
||||
/* воспроизведение: микшированный PCM из сервисного слоя (всегда стерео) */
|
||||
int total = (int)frames * 2;
|
||||
int n = radio_audio_pull_pcm(m_groupId.load(), out16, total);
|
||||
if (n < total)
|
||||
std::memset(out16 + n, 0, (size_t)(total - n) * sizeof(int16_t));
|
||||
|
||||
uint64_t end = get_time_tb();
|
||||
uint64_t body = end - now;
|
||||
if (body > m_dbgBodyMax) m_dbgBodyMax = body;
|
||||
unsigned int offset = 0;
|
||||
while (offset < frames) {
|
||||
unsigned int count = std::min(frames - offset, (unsigned int)RADIO_AUDIO_FRAME_SAMPLES);
|
||||
int n = radio_audio_pull_pcm(m_groupId.load(), out16 + offset * 2, count * 2);
|
||||
if (n < (int)count * 2) std::memset(out16 + offset * 2 + n, 0, (count * 2 - n) * sizeof(*out16));
|
||||
offset += count;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include "audio_device.h"
|
||||
#include <QTimer>
|
||||
|
||||
struct ma_device;
|
||||
|
||||
@@ -43,31 +45,25 @@ signals:
|
||||
void talkingChanged(bool talking); // GUI-поток, суммарное состояние кнопки и хоткея
|
||||
|
||||
private:
|
||||
RadioAudioEngine() = default;
|
||||
RadioAudioEngine();
|
||||
void updateTalking();
|
||||
bool m_buttonTalking = false, m_hotkeyTalking = false; // GUI-поток
|
||||
void teardownDevice();
|
||||
int initDevice();
|
||||
void pauseDevices();
|
||||
void recoverDevices();
|
||||
void openCapture();
|
||||
void openPlayback();
|
||||
|
||||
std::atomic<bool> m_active{false};
|
||||
std::atomic<bool> m_talking{false};
|
||||
std::atomic<bool> m_vadMode{false}; /* VAD авто-PTT: фидим PCM постоянно */
|
||||
std::atomic<uint64_t> m_groupId{0};
|
||||
|
||||
ma_device* m_captureDevice = nullptr;
|
||||
ma_device* m_playbackDevice = nullptr;
|
||||
bool m_captureStarted = false;
|
||||
bool m_playbackStarted = false;
|
||||
AudioDevice m_capture, m_playback;
|
||||
QTimer m_recoveryTimer;
|
||||
bool m_wantStereo = false;
|
||||
QString m_captureId, m_playbackId;
|
||||
|
||||
std::vector<int16_t> m_captureBuf; /* FRAME_SAMPLES * m_captureChannels, аудио-поток */
|
||||
int m_capturePos = 0;
|
||||
int m_captureChannels = 1; /* 1=моно, 2=стерео — по настройке/возможности */
|
||||
|
||||
/* диагностика (DEBUG_CATEGORY_DEBUG): фактический cadence playback-коллбэка */
|
||||
unsigned int m_dbgFrames = 0;
|
||||
uint32_t m_dbgCbCount = 0;
|
||||
uint64_t m_dbgLastTb = 0; /* момент предыдущего сброса статистики (для cb/s) */
|
||||
uint64_t m_dbgPrevCbTb = 0; /* момент входа предыдущего коллбэка */
|
||||
uint64_t m_dbgGapMax = 0; /* макс. gap между коллбэками за окно (tb 0.1мс) */
|
||||
uint64_t m_dbgBodyMax = 0; /* макс. длительность тела коллбэка за окно (tb 0.1мс) */
|
||||
};
|
||||
@@ -1,8 +1,11 @@
|
||||
#include "miniaudio.h"
|
||||
|
||||
#include "sound_manager.h"
|
||||
#include "../transport/audio_diagnostics.h"
|
||||
#include <QFile>
|
||||
#include <QDebug>
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include "../../lib/debug_config.h"
|
||||
|
||||
SoundManager* SoundManager::instance() {
|
||||
@@ -15,30 +18,100 @@ SoundManager::~SoundManager() {
|
||||
shutdown();
|
||||
}
|
||||
|
||||
extern "C" int audio_context_failed(ma_context* context);
|
||||
|
||||
SoundManager::SoundManager() {
|
||||
m_clock.start();
|
||||
m_recoveryTimer.setInterval(200);
|
||||
connect(&m_recoveryTimer, &QTimer::timeout, this, &SoundManager::recoverAudio);
|
||||
}
|
||||
|
||||
bool SoundManager::init() {
|
||||
if (m_initialized) return true;
|
||||
|
||||
ma_engine* engine = new ma_engine;
|
||||
ma_result result = ma_engine_init(NULL, engine);
|
||||
audio_diag_start();
|
||||
ma_engine_config config = ma_engine_config_init();
|
||||
config.noDevice = MA_TRUE; /* Микшер не владеет контекстом/устройством; его PCM и звуки переживают recovery. */
|
||||
config.channels = 2;
|
||||
config.sampleRate = 48000;
|
||||
auto* engine = new ma_engine{};
|
||||
ma_result result = ma_engine_init(&config, engine);
|
||||
if (result != MA_SUCCESS) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "SoundManager: ma_engine_init failed (%d)", result);
|
||||
delete engine;
|
||||
audio_diag_stop();
|
||||
return false;
|
||||
}
|
||||
m_engine = engine;
|
||||
m_context = ma_device_get_context(ma_engine_get_device(engine));
|
||||
m_initialized = true;
|
||||
|
||||
ma_device* dev = ma_engine_get_device(engine);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_GENERAL,
|
||||
"SoundManager: initialized engine=%p ctx=%p dev='%s' fmt=%d ch=%u sr=%u internalSr=%u",
|
||||
(void*)engine, (void*)m_context,
|
||||
dev ? dev->playback.name : "?", dev ? (int)dev->playback.format : -1,
|
||||
dev ? dev->playback.channels : 0, dev ? dev->sampleRate : 0,
|
||||
dev ? dev->playback.internalSampleRate : 0);
|
||||
m_contextRetryAt = 0;
|
||||
recoverAudio();
|
||||
m_recoveryTimer.start();
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "SoundManager: mixer initialized independently of output");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SoundManager::openContext() {
|
||||
auto* context = new ma_context{};
|
||||
ma_result result = ma_context_init(m_backendSelected ? &m_backend : nullptr, m_backendSelected ? 1 : 0, nullptr, context);
|
||||
if (result != MA_SUCCESS) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "SoundManager: context init failed result=%d retry=%dms", result, m_contextRetryDelay);
|
||||
delete context;
|
||||
m_contextRetryAt = m_clock.elapsed() + m_contextRetryDelay;
|
||||
m_contextRetryDelay = std::min(m_contextRetryDelay * 2, 2000);
|
||||
return false;
|
||||
}
|
||||
m_context = context;
|
||||
m_backend = context->backend;
|
||||
m_backendSelected = true;
|
||||
m_contextRetryDelay = 250;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "SoundManager: context ready backend=%s", ma_get_backend_name(m_backend));
|
||||
emit contextRestored();
|
||||
return true;
|
||||
}
|
||||
|
||||
void SoundManager::openOutput() {
|
||||
ma_device_config config = ma_device_config_init(ma_device_type_playback);
|
||||
config.playback.format = ma_format_f32;
|
||||
config.playback.channels = 2;
|
||||
config.sampleRate = 48000;
|
||||
ma_device_id id;
|
||||
bool selected = deviceId(m_playbackId, &id);
|
||||
if (selected) config.playback.pDeviceID = &id;
|
||||
auto callback = [this](const void*, void* out, unsigned int frames) {
|
||||
ma_engine_read_pcm_frames(m_engine, out, frames, nullptr);
|
||||
};
|
||||
if (m_output.init(m_context, config, "sound-engine", 0, callback) && m_output.start()) return;
|
||||
if (!selected) return;
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "SoundManager: selected output unavailable, trying default");
|
||||
config.playback.pDeviceID = nullptr;
|
||||
if (m_output.init(m_context, config, "sound-engine", 0, callback)) m_output.start();
|
||||
}
|
||||
|
||||
void SoundManager::recoverAudio() {
|
||||
if (!m_initialized) return;
|
||||
if (m_context && audio_context_failed(m_context)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "SoundManager: context failed, closing dependent devices");
|
||||
emit contextAboutToReset();
|
||||
m_output.close();
|
||||
ma_context_uninit(m_context); delete m_context; m_context = nullptr;
|
||||
m_contextRetryAt = m_clock.elapsed() + 250;
|
||||
}
|
||||
if (!m_context && m_clock.elapsed() >= m_contextRetryAt) openContext();
|
||||
if (!m_context) return;
|
||||
if (m_output.needsRecovery()) { m_output.close(); m_output.retryLater(); }
|
||||
if (m_output.retryReady()) openOutput();
|
||||
}
|
||||
|
||||
void SoundManager::setDevices(const QString& output, const QString& input) {
|
||||
if (output == m_playbackId && input == m_captureId) return;
|
||||
bool outputChanged = output != m_playbackId;
|
||||
m_playbackId = output; m_captureId = input;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "audio: device selection changed output_default=%d input_default=%d", output.isEmpty(), input.isEmpty());
|
||||
if (outputChanged) { m_output.close(); m_output.retryNow(); }
|
||||
emit devicesChanged();
|
||||
recoverAudio();
|
||||
}
|
||||
|
||||
void SoundManager::clearCurrentPcm() {
|
||||
if (m_currentPcmSound) { ma_sound_uninit(m_currentPcmSound); delete m_currentPcmSound; m_currentPcmSound = nullptr; }
|
||||
if (m_currentPcmBuffer) {
|
||||
@@ -51,7 +124,10 @@ void SoundManager::clearCurrentPcm() {
|
||||
|
||||
void SoundManager::shutdown() {
|
||||
if (!m_initialized) return;
|
||||
|
||||
m_recoveryTimer.stop();
|
||||
m_initialized = false;
|
||||
emit contextAboutToReset();
|
||||
m_output.close();
|
||||
clearCurrentPcm();
|
||||
|
||||
for (auto it = m_sounds.begin(); it != m_sounds.end(); ++it) {
|
||||
@@ -63,7 +139,8 @@ void SoundManager::shutdown() {
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "SoundManager: before ma_engine_uninit engine=%p", (void*)m_engine);
|
||||
if (m_engine) { ma_engine_uninit(m_engine); delete m_engine; m_engine = nullptr; }
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "SoundManager: after ma_engine_uninit");
|
||||
m_initialized = false;
|
||||
if (m_context) { ma_context_uninit(m_context); delete m_context; m_context = nullptr; }
|
||||
audio_diag_stop();
|
||||
m_loopName.clear();
|
||||
}
|
||||
|
||||
@@ -279,6 +356,11 @@ static QString decodeDeviceName(const char* name) {
|
||||
return s;
|
||||
}
|
||||
|
||||
static QString deviceKey(ma_context* context, const ma_device_id& id) {
|
||||
QByteArray bytes(reinterpret_cast<const char*>(&id), sizeof(id));
|
||||
return QString::fromLatin1(ma_get_backend_name(context->backend)) + '/' + QString::fromLatin1(bytes.toBase64());
|
||||
}
|
||||
|
||||
QVector<SoundManager::DeviceInfo> SoundManager::enumeratePlaybackDevices() {
|
||||
QVector<DeviceInfo> result;
|
||||
ma_context* ctx = instance()->m_context;
|
||||
@@ -286,9 +368,10 @@ QVector<SoundManager::DeviceInfo> SoundManager::enumeratePlaybackDevices() {
|
||||
|
||||
ma_device_info* pDevices = nullptr;
|
||||
ma_uint32 count = 0;
|
||||
if (ma_context_get_devices(ctx, &pDevices, &count, nullptr, nullptr) != MA_SUCCESS) return result;
|
||||
ma_result status = ma_context_get_devices(ctx, &pDevices, &count, nullptr, nullptr);
|
||||
if (status != MA_SUCCESS) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio: output enumeration failed result=%d", status); return result; }
|
||||
for (ma_uint32 i = 0; i < count; i++)
|
||||
result.append({decodeDeviceName(pDevices[i].name), pDevices[i].isDefault != MA_FALSE});
|
||||
result.append({decodeDeviceName(pDevices[i].name), pDevices[i].isDefault != MA_FALSE, deviceKey(ctx, pDevices[i].id)});
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -299,8 +382,37 @@ QVector<SoundManager::DeviceInfo> SoundManager::enumerateCaptureDevices() {
|
||||
|
||||
ma_device_info* pDevices = nullptr;
|
||||
ma_uint32 count = 0;
|
||||
if (ma_context_get_devices(ctx, nullptr, nullptr, &pDevices, &count) != MA_SUCCESS) return result;
|
||||
ma_result status = ma_context_get_devices(ctx, nullptr, nullptr, &pDevices, &count);
|
||||
if (status != MA_SUCCESS) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio: input enumeration failed result=%d", status); return result; }
|
||||
for (ma_uint32 i = 0; i < count; i++)
|
||||
result.append({decodeDeviceName(pDevices[i].name), pDevices[i].isDefault != MA_FALSE});
|
||||
result.append({decodeDeviceName(pDevices[i].name), pDevices[i].isDefault != MA_FALSE, deviceKey(ctx, pDevices[i].id)});
|
||||
return result;
|
||||
}
|
||||
|
||||
bool SoundManager::deviceId(const QString& key, ma_device_id* id) {
|
||||
if (key.isEmpty()) return false;
|
||||
ma_context* context = instance()->context();
|
||||
if (!context) return false;
|
||||
QString prefix = QString::fromLatin1(ma_get_backend_name(context->backend)) + '/';
|
||||
QByteArray bytes = QByteArray::fromBase64(key.mid(prefix.size()).toLatin1());
|
||||
if (!key.startsWith(prefix) || bytes.size() != sizeof(*id) || QString::fromLatin1(bytes.toBase64()) != key.mid(prefix.size())) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "audio: invalid or different-backend device ID, using default backend=%s",
|
||||
ma_get_backend_name(context->backend));
|
||||
return false;
|
||||
}
|
||||
std::memcpy(id, bytes.constData(), sizeof(*id));
|
||||
/* Сохранённая строка должна иметь terminator внутри соответствующего поля backend. */
|
||||
bool valid = true;
|
||||
switch (context->backend) {
|
||||
case ma_backend_pulseaudio: valid = std::memchr(id->pulse, 0, sizeof(id->pulse)); break;
|
||||
case ma_backend_alsa: valid = std::memchr(id->alsa, 0, sizeof(id->alsa)); break;
|
||||
case ma_backend_coreaudio: valid = std::memchr(id->coreaudio, 0, sizeof(id->coreaudio)); break;
|
||||
case ma_backend_sndio: valid = std::memchr(id->sndio, 0, sizeof(id->sndio)); break;
|
||||
case ma_backend_audio4: valid = std::memchr(id->audio4, 0, sizeof(id->audio4)); break;
|
||||
case ma_backend_oss: valid = std::memchr(id->oss, 0, sizeof(id->oss)); break;
|
||||
case ma_backend_wasapi: valid = std::find(std::begin(id->wasapi), std::end(id->wasapi), 0) != std::end(id->wasapi); break;
|
||||
default: break;
|
||||
}
|
||||
if (!valid) DEBUG_WARN(DEBUG_CATEGORY_CALL, "audio: unterminated device ID, using default");
|
||||
return valid;
|
||||
}
|
||||
@@ -7,6 +7,8 @@
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <cstdint>
|
||||
#include <QTimer>
|
||||
#include "audio_device.h"
|
||||
|
||||
struct ma_engine;
|
||||
struct ma_context;
|
||||
@@ -40,6 +42,10 @@ public:
|
||||
void stopLoop();
|
||||
|
||||
ma_context* context();
|
||||
static bool deviceId(const QString& key, ma_device_id* id);
|
||||
void setDevices(const QString& output, const QString& input);
|
||||
QString playbackDevice() const { return m_playbackId; }
|
||||
QString captureDevice() const { return m_captureId; }
|
||||
|
||||
/* owner удерживает готовый PCM без копии в GUI; без owner короткие буферы копируются. */
|
||||
void playRawPcm(const int16_t* pcm, int sampleCount, int sampleRate, int channels,
|
||||
@@ -55,15 +61,24 @@ public:
|
||||
struct DeviceInfo {
|
||||
QString name;
|
||||
bool isDefault = false;
|
||||
QString id;
|
||||
};
|
||||
static QVector<DeviceInfo> enumeratePlaybackDevices();
|
||||
static QVector<DeviceInfo> enumerateCaptureDevices();
|
||||
|
||||
signals:
|
||||
void contextAboutToReset(); // Все зависимые устройства закрываются синхронно перед uninit контекста.
|
||||
void contextRestored();
|
||||
void devicesChanged();
|
||||
|
||||
private:
|
||||
SoundManager() = default;
|
||||
SoundManager();
|
||||
~SoundManager();
|
||||
|
||||
void clearCurrentPcm();
|
||||
void recoverAudio();
|
||||
bool openContext();
|
||||
void openOutput();
|
||||
|
||||
struct LoadedSound {
|
||||
QByteArray mp3Data;
|
||||
@@ -73,6 +88,14 @@ private:
|
||||
|
||||
ma_engine* m_engine = nullptr;
|
||||
ma_context* m_context = nullptr;
|
||||
AudioDevice m_output;
|
||||
QTimer m_recoveryTimer;
|
||||
QElapsedTimer m_clock;
|
||||
qint64 m_contextRetryAt = 0;
|
||||
int m_contextRetryDelay = 250;
|
||||
QString m_playbackId, m_captureId;
|
||||
ma_backend m_backend = ma_backend_null;
|
||||
bool m_backendSelected = false;
|
||||
ma_sound* m_currentPcmSound = nullptr;
|
||||
void* m_currentPcmBuffer = nullptr;
|
||||
int m_currentPcmSampleRate = 0;
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Изолированный PipeWire/PulseAudio с виртуальным sink; системный сервер не затрагивается."""
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
|
||||
def wait_for(predicate, seconds=10):
|
||||
deadline = time.monotonic() + seconds
|
||||
while time.monotonic() < deadline:
|
||||
if predicate():
|
||||
return
|
||||
time.sleep(0.02)
|
||||
raise RuntimeError("private audio server/test did not reach expected state")
|
||||
|
||||
|
||||
def main():
|
||||
for executable in ("pipewire", "pipewire-pulse", "wireplumber", "dbus-run-session", "pactl"):
|
||||
if not shutil.which(executable):
|
||||
raise RuntimeError(f"required executable missing: {executable}")
|
||||
binary = Path(sys.argv[1] if len(sys.argv) > 1 else "tools/chatgui/build/test_audio_recovery").resolve()
|
||||
with tempfile.TemporaryDirectory(prefix="utun-pulse-") as root:
|
||||
directory = Path(root)
|
||||
config = directory / "config/wireplumber/wireplumber.conf.d"
|
||||
config.mkdir(parents=True)
|
||||
(config / "test.conf").write_text('''wireplumber.profiles = {
|
||||
main = {
|
||||
monitor.alsa = disabled
|
||||
monitor.alsa-midi = disabled
|
||||
monitor.bluez = disabled
|
||||
monitor.bluez-midi = disabled
|
||||
monitor.v4l2 = disabled
|
||||
monitor.libcamera = disabled
|
||||
}
|
||||
}''')
|
||||
env = os.environ.copy()
|
||||
env.update(XDG_RUNTIME_DIR=root, PIPEWIRE_RUNTIME_DIR=root, XDG_CONFIG_HOME=str(directory / "config"),
|
||||
XDG_STATE_HOME=str(directory / "state"), PIPEWIRE_REMOTE="pipewire-0", PULSE_SERVER=f"unix:{root}/pulse/native")
|
||||
processes, logs = [], []
|
||||
|
||||
def launch(command, label):
|
||||
log = (directory / f"{label}.log").open("w+")
|
||||
logs.append(log)
|
||||
process = subprocess.Popen(command, env=env, stdout=log, stderr=subprocess.STDOUT, start_new_session=True)
|
||||
processes.append(process)
|
||||
return process
|
||||
|
||||
def pulse_ready():
|
||||
return subprocess.run(["pactl", "info"], env=env, stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL, timeout=2).returncode == 0
|
||||
|
||||
try:
|
||||
launch(["pipewire"], "pipewire")
|
||||
wait_for(lambda: (directory / "pipewire-0").exists())
|
||||
launch(["dbus-run-session", "--", "wireplumber"], "wireplumber")
|
||||
pulse = launch(["pipewire-pulse"], "pulse-before")
|
||||
wait_for(pulse_ready)
|
||||
# module-always-sink создаёт виртуальный dummy sink и его monitor source.
|
||||
test = launch([str(binary), "--pulse-integration", root], "client")
|
||||
wait_for(lambda: (directory / "ready").exists() or test.poll() is not None)
|
||||
if test.poll() is not None:
|
||||
raise RuntimeError("client failed before server restart")
|
||||
pulse.terminate(); pulse.wait(timeout=5)
|
||||
wait_for(lambda: (directory / "closed").exists() or test.poll() is not None)
|
||||
launch(["pipewire-pulse"], "pulse-after")
|
||||
wait_for(pulse_ready)
|
||||
if test.wait(timeout=15) != 0:
|
||||
raise RuntimeError("client recovery failed")
|
||||
logs[3].flush(); logs[3].seek(0)
|
||||
print(logs[3].read())
|
||||
print("private PulseAudio server restart: PASS")
|
||||
except Exception:
|
||||
for log in logs:
|
||||
log.flush(); log.seek(0)
|
||||
print(f"--- {Path(log.name).name} ---\n{log.read()}", file=sys.stderr)
|
||||
raise
|
||||
finally:
|
||||
for process in reversed(processes):
|
||||
if process.poll() is None:
|
||||
os.killpg(process.pid, signal.SIGTERM)
|
||||
try:
|
||||
process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
os.killpg(process.pid, signal.SIGKILL); process.wait()
|
||||
for log in logs:
|
||||
log.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,42 @@
|
||||
/* Проверяем настоящую очередь: конкуренция, переполнение, целостность и повторный запуск. */
|
||||
#include "../transport/audio_diagnostics.cpp"
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
#include <vector>
|
||||
|
||||
int main() {
|
||||
debug_config_init(); debug_enable_console(0);
|
||||
constexpr int producers = 4, perProducer = 20000;
|
||||
std::vector<bool> seen(producers * perProducer);
|
||||
std::atomic<int> done{0};
|
||||
for (uint64_t i = 0; i < kCapacity; ++i) queue[i].sequence.store(i);
|
||||
running = true;
|
||||
// Сначала полное кольцо: producer обязан вернуть управление, потеря учтена.
|
||||
for (uint64_t i = 0; i < kCapacity + 1; ++i) audio_diag_event(nullptr, AUDIO_DIAG_HOLE, AUDIO_DIAG_READ, i, 7);
|
||||
assert(lost.load() == 1);
|
||||
Event e;
|
||||
for (uint64_t i = 0; i < kCapacity; ++i) {
|
||||
assert(take(e)); assert(e.value == int64_t(i) && e.detail == 7);
|
||||
}
|
||||
assert(!take(e)); lost = 0;
|
||||
std::vector<std::thread> workers;
|
||||
for (int p = 0; p < producers; ++p) workers.emplace_back([p, &done]() {
|
||||
for (int i = 0; i < perProducer; ++i)
|
||||
audio_diag_event(nullptr, AUDIO_DIAG_HOLE, AUDIO_DIAG_READ, p * perProducer + i, p);
|
||||
++done;
|
||||
});
|
||||
uint64_t received = 0;
|
||||
while (done.load() != producers || readPosition != writePosition.load()) {
|
||||
if (!take(e)) { std::this_thread::yield(); continue; }
|
||||
assert(e.value >= 0 && e.value < producers * perProducer);
|
||||
assert(e.kind == AUDIO_DIAG_HOLE && e.stage == AUDIO_DIAG_READ && e.value / perProducer == e.detail);
|
||||
assert(e.wall && e.cpu && e.tid && !seen[e.value]);
|
||||
seen[e.value] = true; ++received;
|
||||
}
|
||||
for (auto& worker : workers) worker.join();
|
||||
assert(received + lost.load() == producers * perProducer);
|
||||
running = false;
|
||||
// Lifecycle не оставляет joinable-поток и очищает очередь между сеансами.
|
||||
for (int i = 0; i < 2; ++i) { audio_diag_start(); audio_diag_event(nullptr, AUDIO_DIAG_OPEN, -1, 1, 48000); audio_diag_stop(); }
|
||||
puts("test_audio_diagnostics: bounded MPSC, concurrent integrity and restart PASS");
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
/* Настоящие null devices и Qt timers; linker wrappers инъецируют только сбои lifecycle. */
|
||||
#include "../src/audio_device.h"
|
||||
#include "../src/call_audio_engine.h"
|
||||
#include "../src/radio_audio_engine.h"
|
||||
#include "../src/audiorecorder.h"
|
||||
#include "../src/sound_manager.h"
|
||||
#include <QCoreApplication>
|
||||
#include <QThread>
|
||||
#include <QFile>
|
||||
#include <cstring>
|
||||
#include <algorithm>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
extern "C" {
|
||||
#include "utun_instance.h"
|
||||
#include "call/call_audio.h"
|
||||
#include "radio/radio_audio.h"
|
||||
#include "media_async/attachment_send.h"
|
||||
#include "u_async.h"
|
||||
#include "debug_config.h"
|
||||
}
|
||||
|
||||
#define REQUIRE(condition) do { if (!(condition)) qFatal("audio recovery line %d: %s", __LINE__, #condition); } while (0)
|
||||
static UTUN_INSTANCE instance{};
|
||||
static std::map<ma_context*, int> live;
|
||||
static std::vector<ma_device*> devices;
|
||||
static int starts, captureStarts, playbackStarts, contextStarts, failContexts;
|
||||
static bool failStart, failContext, failDevice;
|
||||
static bool pulseIntegration;
|
||||
|
||||
extern "C" UTUN_INSTANCE* gui_bridge_get_inst() { return &instance; }
|
||||
extern "C" ma_result __real_ma_context_init(const ma_backend*, ma_uint32, const ma_context_config*, ma_context*);
|
||||
extern "C" void __real_ma_context_uninit(ma_context*);
|
||||
extern "C" ma_result __real_ma_device_init(ma_context*, const ma_device_config*, ma_device*);
|
||||
extern "C" ma_result __real_ma_device_start(ma_device*);
|
||||
extern "C" void __real_ma_device_uninit(ma_device*);
|
||||
extern "C" int __real_audio_context_failed(ma_context*);
|
||||
|
||||
extern "C" ma_result __wrap_ma_context_init(const ma_backend*, ma_uint32, const ma_context_config*, ma_context* context) {
|
||||
++contextStarts;
|
||||
if (failContexts > 0) { --failContexts; return MA_ERROR; }
|
||||
ma_backend backend = pulseIntegration ? ma_backend_pulseaudio : ma_backend_null;
|
||||
return __real_ma_context_init(&backend, 1, nullptr, context);
|
||||
}
|
||||
extern "C" void __wrap_ma_context_uninit(ma_context* context) {
|
||||
REQUIRE(live[context] == 0);
|
||||
__real_ma_context_uninit(context);
|
||||
}
|
||||
extern "C" ma_result __wrap_ma_device_init(ma_context* context, const ma_device_config* config, ma_device* device) {
|
||||
if (failDevice) { failDevice = false; return MA_ERROR; }
|
||||
ma_result result = __real_ma_device_init(context, config, device);
|
||||
if (result == MA_SUCCESS) { ++live[context]; devices.push_back(device); }
|
||||
return result;
|
||||
}
|
||||
extern "C" ma_result __wrap_ma_device_start(ma_device* device) {
|
||||
++starts;
|
||||
if (device->type == ma_device_type_capture) {
|
||||
++captureStarts;
|
||||
// Некоторые backends вызывают data callback ещё до возврата start.
|
||||
std::vector<int16_t> input(1200 * device->capture.channels, 1000);
|
||||
if (!pulseIntegration) device->onData(device, nullptr, input.data(), 1200);
|
||||
} else ++playbackStarts;
|
||||
if (failStart) { failStart = false; return MA_ERROR; }
|
||||
return __real_ma_device_start(device);
|
||||
}
|
||||
extern "C" void __wrap_ma_device_uninit(ma_device* device) {
|
||||
auto context = device->pContext;
|
||||
__real_ma_device_uninit(device);
|
||||
REQUIRE(live[context] > 0);
|
||||
--live[context];
|
||||
devices.erase(std::find(devices.begin(), devices.end(), device));
|
||||
}
|
||||
extern "C" int __wrap_audio_context_failed(ma_context* context) {
|
||||
if (failContext) { failContext = false; return 1; }
|
||||
return __real_audio_context_failed(context);
|
||||
}
|
||||
|
||||
static void pump(int milliseconds) {
|
||||
QElapsedTimer timer; timer.start();
|
||||
while (timer.elapsed() < milliseconds) { QCoreApplication::processEvents(); QThread::msleep(1); }
|
||||
}
|
||||
|
||||
static void pulse_recovery(SoundManager* manager, const QString& control) {
|
||||
REQUIRE(manager->context()->backend == ma_backend_pulseaudio);
|
||||
AudioRecorder recorder; recorder.init(); recorder.startRecording();
|
||||
std::vector<int16_t> pcm(48000 * 20, 1000);
|
||||
manager->playRawPcm(pcm.data(), (int)pcm.size(), 48000, 1);
|
||||
for (int i = 0; i < 100 && recorder.durationMs() < 300; ++i) pump(50);
|
||||
REQUIRE(recorder.durationMs() >= 300 && manager->currentPcmCursor() > 0);
|
||||
int duration = recorder.durationMs();
|
||||
auto cursor = manager->currentPcmCursor();
|
||||
QFile ready(control + "/ready"); REQUIRE(ready.open(QIODevice::WriteOnly)); ready.close();
|
||||
for (int i = 0; i < 100 && manager->context(); ++i) pump(50);
|
||||
REQUIRE(!manager->context());
|
||||
QFile closed(control + "/closed"); REQUIRE(closed.open(QIODevice::WriteOnly)); closed.close();
|
||||
for (int i = 0; i < 150 && (!manager->context() || recorder.durationMs() <= duration + 200); ++i) pump(50);
|
||||
REQUIRE(manager->context() && recorder.durationMs() > duration + 200 && manager->currentPcmCursor() > cursor);
|
||||
qInfo("PulseAudio progress: capture=%d -> %dms playback=%llu -> %llu frames", duration, recorder.durationMs(),
|
||||
(unsigned long long)cursor, manager->currentPcmCursor());
|
||||
recorder.stopRecording(); manager->shutdown();
|
||||
REQUIRE(devices.empty());
|
||||
qInfo("real PulseAudio: capture/playback progress restored after server restart PASS");
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
QCoreApplication app(argc, argv);
|
||||
pulseIntegration = argc == 3 && std::strcmp(argv[1], "--pulse-integration") == 0;
|
||||
debug_config_init(); debug_set_level(pulseIntegration ? DEBUG_LEVEL_INFO : DEBUG_LEVEL_WARN);
|
||||
auto* manager = SoundManager::instance();
|
||||
REQUIRE(manager->init() && manager->context());
|
||||
if (pulseIntegration) { pulse_recovery(manager, QString::fromLocal8Bit(argv[2])); return 0; }
|
||||
ma_device_id id{};
|
||||
auto outputs = manager->enumeratePlaybackDevices();
|
||||
REQUIRE(!outputs.empty() && SoundManager::deviceId(outputs.front().id, &id));
|
||||
REQUIRE(!SoundManager::deviceId(outputs.front().id + '!', &id));
|
||||
{
|
||||
AudioDevice device;
|
||||
auto config = ma_device_config_init(ma_device_type_capture);
|
||||
config.capture.format = ma_format_s16; config.capture.channels = 1; config.sampleRate = 48000;
|
||||
REQUIRE(device.init(manager->context(), config, "test-partial-start", 0, [](const void*, void*, unsigned int) {}));
|
||||
int count = live[manager->context()];
|
||||
failStart = true;
|
||||
REQUIRE(!device.start() && !device.initialized());
|
||||
REQUIRE(live[manager->context()] == count - 1);
|
||||
failDevice = true;
|
||||
REQUIRE(!device.init(manager->context(), config, "test-init-failure", 0, [](const void*, void*, unsigned int) {}));
|
||||
REQUIRE(!device.initialized() && live[manager->context()] == count - 1);
|
||||
}
|
||||
{
|
||||
AudioRecorder first, second;
|
||||
first.init(); second.init();
|
||||
auto inputs = manager->enumerateCaptureDevices();
|
||||
REQUIRE(!inputs.empty());
|
||||
first.setCaptureDevice(inputs.front().id);
|
||||
int previous = captureStarts;
|
||||
failStart = true;
|
||||
first.startRecording(); second.startRecording();
|
||||
REQUIRE(captureStarts == previous + 3); // Выбранный input не стартовал: default запущен после полного uninit.
|
||||
pump(60);
|
||||
first.stopRecording(); second.stopRecording();
|
||||
attachment_send_req a{}, b{};
|
||||
REQUIRE(first.takeRecording(&a) && second.takeRecording(&b));
|
||||
REQUIRE(a.pcm_count >= 1200 && b.pcm_count >= 1200);
|
||||
REQUIRE(a.pcm[0] == 1000 && b.pcm[0] == 1000); // pUserData маршрутизирует каждый callback своему recorder.
|
||||
a.pcm_release(a.pcm_owner); b.pcm_release(b.pcm_owner);
|
||||
}
|
||||
instance.ua = uasync_create(); REQUIRE(instance.ua);
|
||||
REQUIRE(call_audio_init(&instance) == 0 && radio_audio_init(&instance) == 0);
|
||||
auto* call = CallAudioEngine::instance();
|
||||
REQUIRE(call->start(42) == 0);
|
||||
pump(60);
|
||||
ma_device* playback = nullptr;
|
||||
for (auto* device : devices)
|
||||
if (device->type == ma_device_type_playback && device->playback.format == ma_format_s16 && device->playback.channels == 1)
|
||||
playback = device;
|
||||
REQUIRE(playback);
|
||||
int previousCapture = captureStarts, previousPlayback = playbackStarts;
|
||||
REQUIRE(ma_device_stop(playback) == MA_SUCCESS);
|
||||
pump(800);
|
||||
REQUIRE(captureStarts == previousCapture && playbackStarts > previousPlayback);
|
||||
REQUIRE(call->callId() == 42 && call->isActive());
|
||||
auto* radio = RadioAudioEngine::instance();
|
||||
REQUIRE(radio->start(7) == 0);
|
||||
AudioRecorder recorder; recorder.init(); recorder.startRecording();
|
||||
std::vector<int16_t> notification(48000 * 10, 1000);
|
||||
manager->playRawPcm(notification.data(), (int)notification.size(), 48000, 1);
|
||||
pump(60);
|
||||
auto cursor = manager->currentPcmCursor();
|
||||
REQUIRE(cursor > 0);
|
||||
int previousContext = contextStarts;
|
||||
failContext = true; failContexts = 2;
|
||||
pump(1800);
|
||||
REQUIRE(contextStarts >= previousContext + 3 && manager->context());
|
||||
REQUIRE(call->callId() == 42 && radio->groupId() == 7 && recorder.isRecording());
|
||||
REQUIRE(manager->currentPcmCursor() > cursor && manager->isCurrentPcmPlaying());
|
||||
call->stop(); radio->stop(); recorder.stopRecording();
|
||||
int previousStarts = starts;
|
||||
pump(600);
|
||||
REQUIRE(starts == previousStarts); // Отложенное recovery не воскрешает остановленную сессию.
|
||||
REQUIRE(call->start(43) == 0);
|
||||
call->beginEnd(43);
|
||||
REQUIRE(call->start(43) == 0);
|
||||
pump(600);
|
||||
REQUIRE(call->isActive() && call->callId() == 43); // Старый end timer не закрывает новое поколение того же call ID.
|
||||
call->stop();
|
||||
failContext = true; failContexts = 1;
|
||||
recorder.startRecording();
|
||||
pump(220);
|
||||
REQUIRE(!manager->context());
|
||||
recorder.stopRecording();
|
||||
previousCapture = captureStarts;
|
||||
pump(1000);
|
||||
REQUIRE(manager->context() && captureStarts == previousCapture);
|
||||
|
||||
// Предел TX действует до помещения задачи в uasync; старое поколение не отправляется после stop/start.
|
||||
uasync_poll(instance.ua, 0);
|
||||
REQUIRE(call_audio_start(99) == 0);
|
||||
int16_t silence[960]{};
|
||||
for (int i = 0; i < 100; ++i) call_audio_feed_pcm(99, silence, 960);
|
||||
int pending = 0;
|
||||
for (auto* task = instance.ua->posted_tasks_head; task; task = task->next) ++pending;
|
||||
REQUIRE(pending == 8);
|
||||
call_audio_stop();
|
||||
REQUIRE(call_audio_start(100) == 0);
|
||||
for (int i = 0; i < 100; ++i) call_audio_feed_pcm(100, silence, 960);
|
||||
pending = 0;
|
||||
for (auto* task = instance.ua->posted_tasks_head; task; task = task->next) ++pending;
|
||||
REQUIRE(pending == 8); // Новый звонок также учитывает ещё не освобождённые задачи старого.
|
||||
call_audio_stop();
|
||||
uasync_poll(instance.ua, 0);
|
||||
REQUIRE(!instance.ua->posted_tasks_head);
|
||||
REQUIRE(radio_audio_start(8, 1) == 0);
|
||||
radio_audio_talk_begin(8); radio_audio_talk_end(8); radio_audio_stop();
|
||||
REQUIRE(instance.ua->posted_tasks_head);
|
||||
// Завершение ядра освобождает ещё не выполненные PTT-задачи вместе с аргументами.
|
||||
call_audio_destroy(&instance); radio_audio_destroy(&instance);
|
||||
uasync_destroy(instance.ua, 0); instance.ua = nullptr;
|
||||
manager->shutdown();
|
||||
REQUIRE(devices.empty());
|
||||
qInfo("audio recovery: partial start, independent streams, two recorders, context restart, cancellation, bounded TX PASS");
|
||||
return 0;
|
||||
}
|
||||
@@ -20,6 +20,11 @@ static int decode(void* arg, const uint8_t* enc, int len, int16_t* pcm, int cap)
|
||||
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);
|
||||
}
|
||||
@@ -247,6 +252,18 @@ int main() {
|
||||
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);
|
||||
}
|
||||
std::printf("call_jitter: %s\n", failures ? "FAILED" : "PASSED");
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
/* Реальные ветки miniaudio на null backend: аппаратный звук и сеть не нужны. */
|
||||
#include "../transport/audio_diagnostics.h"
|
||||
#include "../../../lib/speex_aec.h"
|
||||
#include <assert.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
static uint64_t observed_unfilled, observed_no_progress, observed_holes;
|
||||
static uint32_t callback_count;
|
||||
static void test_event(const void* device, int kind, int stage, int64_t value, int64_t detail) {
|
||||
if (kind == AUDIO_DIAG_UNFILLED) observed_unfilled += (uint64_t)value;
|
||||
if (kind == AUDIO_DIAG_NO_PROGRESS) observed_no_progress += (uint64_t)value;
|
||||
if (kind == AUDIO_DIAG_HOLE) observed_holes += (uint64_t)value;
|
||||
audio_diag_event(device, kind, stage, value, detail);
|
||||
}
|
||||
#define MA_DIAGNOSTIC_EVENT(device, kind, stage, value, detail) test_event(device, kind, stage, value, detail)
|
||||
#define MA_DIAGNOSTIC_LOG(log, level, message) audio_diag_log(log, level, message)
|
||||
#define MA_PULSE_WAIT_TIMEOUT_MS 20
|
||||
#define MINIAUDIO_IMPLEMENTATION
|
||||
#include "../../../lib/miniaudio.h"
|
||||
|
||||
static void callback(ma_device* device, void* out, const void* in, ma_uint32 count) {
|
||||
(void)device; (void)in;
|
||||
callback_count++;
|
||||
if (out) for (ma_uint32 i = 0; i < count; i++) ((int16_t*)out)[i] = 12000;
|
||||
}
|
||||
|
||||
static void starvation(int duplex) {
|
||||
ma_backend backend = ma_backend_null;
|
||||
ma_context context;
|
||||
ma_device device;
|
||||
ma_device_config config = ma_device_config_init(duplex ? ma_device_type_duplex : ma_device_type_playback);
|
||||
config.capture.format = config.playback.format = ma_format_s16;
|
||||
config.capture.channels = config.playback.channels = 1;
|
||||
config.sampleRate = 48000; config.periodSizeInFrames = 480; config.dataCallback = callback;
|
||||
assert(ma_context_init(&backend, 1, NULL, &context) == MA_SUCCESS);
|
||||
assert(ma_device_init(&context, &config, &device) == MA_SUCCESS);
|
||||
if (duplex) assert(ma_duplex_rb_init(ma_format_s16, 1, 48000, 48000, 480, NULL, &device.duplexRB) == MA_SUCCESS);
|
||||
/* Worker не запускаем: стенд синхронно вызывает backend-обработчик. */
|
||||
ma_device__set_state(&device, ma_device_state_started);
|
||||
audio_diag_label(&device, duplex ? 1 : 2, duplex ? "test-duplex" : "test-playback");
|
||||
int16_t input[480] = {0}, output[480];
|
||||
observed_unfilled = 0; callback_count = 0;
|
||||
uint64_t untouched = 0;
|
||||
for (int tick = 0; tick < 20; tick++) {
|
||||
if (duplex && tick < 3) assert(ma_device_handle_backend_data_callback(&device, NULL, input, 480) == MA_SUCCESS);
|
||||
for (int i = 0; i < 480; i++) output[i] = 2345;
|
||||
assert(ma_device_handle_backend_data_callback(&device, output, NULL, 480) == MA_SUCCESS);
|
||||
for (int i = 0; i < 480; i++) untouched += output[i] == 2345;
|
||||
}
|
||||
assert(untouched == observed_unfilled);
|
||||
assert(untouched == 0 && callback_count >= 20);
|
||||
printf("starvation duplex=%d unfilled=%llu callback_count=%u PASS\n", duplex,
|
||||
(unsigned long long)untouched, callback_count);
|
||||
ma_device__set_state(&device, ma_device_state_stopped);
|
||||
if (duplex) ma_duplex_rb_uninit(&device.duplexRB);
|
||||
ma_device_uninit(&device); ma_context_uninit(&context);
|
||||
}
|
||||
|
||||
#ifdef MA_SUPPORT_PULSEAUDIO
|
||||
static size_t zero_writable(const ma_pa_stream* stream) {
|
||||
(void)stream;
|
||||
/* Без внешнего stop callback обязан вернуть управление сам. */
|
||||
return 0;
|
||||
}
|
||||
static void no_progress(void) {
|
||||
ma_context context;
|
||||
ma_device device;
|
||||
memset(&context, 0, sizeof(context)); memset(&device, 0, sizeof(device));
|
||||
device.pContext = &context;
|
||||
device.playback.internalFormat = ma_format_s16; device.playback.internalChannels = 1;
|
||||
context.pulse.pa_stream_writable_size = (ma_proc)zero_writable;
|
||||
ma_device__set_state(&device, ma_device_state_started);
|
||||
observed_no_progress = 0;
|
||||
ma_device_on_write__pulse((ma_pa_stream*)&device, 960, &device);
|
||||
assert(observed_no_progress == 1);
|
||||
audio_diag_event(&device, AUDIO_DIAG_CLOSE, -1, 0, 0);
|
||||
puts("PulseAudio zero-progress detection PASS");
|
||||
}
|
||||
static unsigned int wait_cancelled, wait_unrefed;
|
||||
static ma_pa_operation_state_t wait_state(const ma_pa_operation* operation) { (void)operation; return MA_PA_OPERATION_RUNNING; }
|
||||
static void wait_cancel(ma_pa_operation* operation) { (void)operation; ++wait_cancelled; }
|
||||
static void wait_unref(ma_pa_operation* operation) { (void)operation; ++wait_unrefed; }
|
||||
static int wait_iterate(ma_pa_mainloop* loop, int block, int* result) {
|
||||
(void)loop; (void)result;
|
||||
assert(block == 0);
|
||||
return 0;
|
||||
}
|
||||
static void bounded_wait(void) {
|
||||
ma_context context;
|
||||
ma_timer timer;
|
||||
memset(&context, 0, sizeof(context));
|
||||
context.pulse.pa_operation_get_state = (ma_proc)wait_state;
|
||||
context.pulse.pa_operation_cancel = (ma_proc)wait_cancel;
|
||||
context.pulse.pa_operation_unref = (ma_proc)wait_unref;
|
||||
context.pulse.pa_mainloop_iterate = (ma_proc)wait_iterate;
|
||||
wait_cancelled = wait_unrefed = 0;
|
||||
ma_timer_init(&timer);
|
||||
assert(ma_wait_for_operation_and_unref__pulse(&context, NULL, (ma_pa_operation*)&context) == MA_TIMEOUT);
|
||||
assert(wait_cancelled == 1 && wait_unrefed == 1);
|
||||
assert(ma_timer_get_time_in_seconds(&timer) < 0.5);
|
||||
puts("PulseAudio deadline and callback cancellation PASS");
|
||||
}
|
||||
|
||||
static unsigned int hole_frames, hole_drops;
|
||||
static int hole_peek(ma_pa_stream* stream, const void** data, size_t* bytes) {
|
||||
(void)stream; *data = NULL; *bytes = 9600; return 0;
|
||||
}
|
||||
static int hole_drop(ma_pa_stream* stream) { (void)stream; ++hole_drops; return 0; }
|
||||
static void hole_capture(ma_device* device, void* out, const void* in, ma_uint32 frames) {
|
||||
(void)device; (void)out;
|
||||
assert(in);
|
||||
for (ma_uint32 i = 0; i < frames; ++i) assert(((const int16_t*)in)[i] == 0);
|
||||
hole_frames += frames;
|
||||
}
|
||||
static void capture_hole(void) {
|
||||
ma_backend backend = ma_backend_null;
|
||||
ma_context context;
|
||||
ma_device device;
|
||||
ma_device_config config = ma_device_config_init(ma_device_type_capture);
|
||||
config.capture.format = ma_format_s16; config.capture.channels = 1; config.sampleRate = 48000;
|
||||
config.noFixedSizedCallback = MA_TRUE; config.dataCallback = hole_capture;
|
||||
assert(ma_context_init(&backend, 1, NULL, &context) == MA_SUCCESS);
|
||||
assert(ma_device_init(&context, &config, &device) == MA_SUCCESS);
|
||||
context.pulse.pa_stream_peek = (ma_proc)hole_peek;
|
||||
context.pulse.pa_stream_drop = (ma_proc)hole_drop;
|
||||
observed_holes = hole_frames = hole_drops = 0;
|
||||
ma_device__set_state(&device, ma_device_state_started);
|
||||
ma_device_on_read__pulse((ma_pa_stream*)&device, 9600, &device);
|
||||
assert(observed_holes == 4800 && hole_frames == 4800 && hole_drops == 1);
|
||||
ma_device__set_state(&device, ma_device_state_stopped);
|
||||
ma_device_uninit(&device); ma_context_uninit(&context);
|
||||
puts("PulseAudio capture hole preserves duration and silence PASS");
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
/* Характеризация найденной потери хвоста; после исправления ожидаем два кадра. */
|
||||
static void aec_variable_chunks(void) {
|
||||
speex_aec_t* aec = speex_aec_create(48000, 960, 14400, 8);
|
||||
int16_t pcm[1920] = {0};
|
||||
assert(aec != NULL);
|
||||
speex_aec_feed_playback(aec, pcm, 1200);
|
||||
speex_aec_feed_playback(aec, pcm + 1200, 720);
|
||||
const int frames = speex_aec_delay_fill(aec);
|
||||
assert(frames == 2);
|
||||
printf("AEC characterization: input=1920 queued=%d*960; all samples preserved\n", frames);
|
||||
speex_aec_destroy(aec);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
audio_diag_start();
|
||||
starvation(1); starvation(0);
|
||||
#ifdef MA_SUPPORT_PULSEAUDIO
|
||||
no_progress();
|
||||
bounded_wait();
|
||||
capture_hole();
|
||||
#endif
|
||||
aec_variable_chunks();
|
||||
audio_diag_stop();
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
#include "audio_diagnostics.h"
|
||||
|
||||
#ifdef __linux__
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <set>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
#include <thread>
|
||||
#include <time.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <unistd.h>
|
||||
|
||||
extern "C" {
|
||||
#include "../../../lib/debug_config.h"
|
||||
}
|
||||
|
||||
namespace {
|
||||
constexpr uint64_t kCapacity = 8192;
|
||||
constexpr uint64_t kSecond = 1000000000;
|
||||
const char* const kFaults[] = {"hole", "unfilled", "capture-drop", "no-progress", "backend-underrun", "backend-overflow", "error"};
|
||||
const char* const kStages[] = {"loop", "read", "write", "map", "submit", "duplex", "callback", "feed", "pull"};
|
||||
static_assert(std::atomic<uint64_t>::is_always_lock_free);
|
||||
|
||||
struct Event {
|
||||
uintptr_t device;
|
||||
uint64_t wall, cpu;
|
||||
int64_t value, detail;
|
||||
int kind, stage;
|
||||
long tid;
|
||||
char text[192];
|
||||
};
|
||||
struct Slot { std::atomic<uint64_t> sequence{0}; Event event{}; };
|
||||
std::array<Slot, kCapacity> queue;
|
||||
std::atomic<uint64_t> writePosition{0}, lost{0};
|
||||
std::atomic<bool> running{false};
|
||||
uint64_t readPosition = 0;
|
||||
std::thread reporter;
|
||||
|
||||
uint64_t now(clockid_t clock) {
|
||||
timespec ts{};
|
||||
clock_gettime(clock, &ts);
|
||||
return uint64_t(ts.tv_sec) * kSecond + uint64_t(ts.tv_nsec);
|
||||
}
|
||||
|
||||
/* Ограниченная MPSC-очередь: producer не ждёт, не выделяет память и не пишет лог. */
|
||||
void publish(const void* device, int kind, int stage, int64_t value, int64_t detail, const char* text) {
|
||||
if (!running.load(std::memory_order_relaxed)) return;
|
||||
const auto wall = now(CLOCK_MONOTONIC), cpu = now(CLOCK_THREAD_CPUTIME_ID);
|
||||
uint64_t pos = writePosition.load(std::memory_order_relaxed);
|
||||
for (int attempt = 0; attempt < 4; ++attempt) {
|
||||
auto& slot = queue[pos % kCapacity];
|
||||
const auto seq = slot.sequence.load(std::memory_order_acquire);
|
||||
if (seq == pos && writePosition.compare_exchange_weak(pos, pos + 1, std::memory_order_relaxed)) {
|
||||
auto& e = slot.event;
|
||||
e.device = reinterpret_cast<uintptr_t>(device); e.wall = wall; e.cpu = cpu;
|
||||
e.kind = kind; e.stage = stage; e.value = value; e.detail = detail;
|
||||
e.tid = syscall(SYS_gettid);
|
||||
e.text[0] = 0;
|
||||
if (text) {
|
||||
std::strncpy(e.text, text, sizeof(e.text) - 1);
|
||||
e.text[sizeof(e.text) - 1] = 0;
|
||||
}
|
||||
slot.sequence.store(pos + 1, std::memory_order_release);
|
||||
return;
|
||||
}
|
||||
if (seq < pos) break; // Полное кольцо.
|
||||
pos = writePosition.load(std::memory_order_relaxed);
|
||||
}
|
||||
lost.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
bool take(Event& event) {
|
||||
auto& slot = queue[readPosition % kCapacity];
|
||||
if (slot.sequence.load(std::memory_order_acquire) != readPosition + 1) return false;
|
||||
event = slot.event;
|
||||
slot.sequence.store(readPosition + kCapacity, std::memory_order_release);
|
||||
++readPosition;
|
||||
return true;
|
||||
}
|
||||
|
||||
struct Stage {
|
||||
uint64_t entered = 0, cpu = 0, last = 0, calls = 0, frames = 0, maxGap = 0, maxWork = 0, maxCpu = 0;
|
||||
int64_t input = 0, output = 0;
|
||||
long tid = 0;
|
||||
};
|
||||
struct Device {
|
||||
std::string label = "unlabelled";
|
||||
uint64_t id = 0;
|
||||
std::array<Stage, AUDIO_DIAG_STAGE_COUNT> stages{};
|
||||
std::array<uint64_t, AUDIO_DIAG_LOG + 1> faults{};
|
||||
};
|
||||
|
||||
void consume(const Event& e, std::map<uintptr_t, Device>& devices) {
|
||||
if (e.kind == AUDIO_DIAG_LOG) {
|
||||
// Hole учитывается кадрами в read-ветке; строка на каждый hole засоряет лог.
|
||||
if (std::strstr(e.text, "Hole.")) return;
|
||||
std::string message(e.text);
|
||||
while (!message.empty() && (message.back() == '\n' || message.back() == '\r')) message.pop_back();
|
||||
if (e.value == 1) DEBUG_ERROR(DEBUG_CATEGORY_CALL, "AudioDiag: miniaudio log=%p %s", (void*)e.device, message.c_str());
|
||||
else if (e.value == 2) DEBUG_WARN(DEBUG_CATEGORY_CALL, "AudioDiag: miniaudio log=%p level=%lld %s",
|
||||
(void*)e.device, (long long)e.value, message.c_str());
|
||||
else if (e.value == 3) DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "AudioDiag: miniaudio log=%p %s", (void*)e.device, message.c_str());
|
||||
else DEBUG_TRACE(DEBUG_CATEGORY_CALL, "AudioDiag: miniaudio log=%p %s", (void*)e.device, message.c_str());
|
||||
return;
|
||||
}
|
||||
if (e.kind == AUDIO_DIAG_CLOSE) {
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "AudioDiag: close dev=%p mono_ms=%llu", (void*)e.device, (unsigned long long)(e.wall / 1000000));
|
||||
devices.erase(e.device);
|
||||
return;
|
||||
}
|
||||
auto& d = devices[e.device];
|
||||
if (e.kind == AUDIO_DIAG_LABEL) {
|
||||
d.label = e.text; d.id = uint64_t(e.value);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "AudioDiag: label dev=%p role=%s id=%016llx", (void*)e.device, e.text, (unsigned long long)d.id);
|
||||
} else if (e.kind == AUDIO_DIAG_OPEN) {
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "AudioDiag: open dev=%p type=%lld rate=%lld mono_ms=%llu", (void*)e.device,
|
||||
(long long)e.value, (long long)e.detail, (unsigned long long)(e.wall / 1000000));
|
||||
} else if (e.kind == AUDIO_DIAG_BACKEND_STARTED) {
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "AudioDiag: backend playback started/resumed dev=%p mono_ms=%llu",
|
||||
(void*)e.device, (unsigned long long)(e.wall / 1000000));
|
||||
} else if (e.kind == AUDIO_DIAG_NOTIFY) {
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "AudioDiag: notification dev=%p type=%lld state=%lld mono_ms=%llu", (void*)e.device,
|
||||
(long long)e.value, (long long)e.detail, (unsigned long long)(e.wall / 1000000));
|
||||
} else if (e.kind == AUDIO_DIAG_ENTER && e.stage >= 0 && e.stage < AUDIO_DIAG_STAGE_COUNT) {
|
||||
auto& s = d.stages[e.stage];
|
||||
s.input = e.detail;
|
||||
if (s.last && e.wall >= s.last) s.maxGap = std::max(s.maxGap, e.wall - s.last);
|
||||
s.last = s.entered = e.wall; s.cpu = e.cpu; s.tid = e.tid;
|
||||
++s.calls;
|
||||
if (e.value > 0) s.frames += uint64_t(e.value);
|
||||
} else if (e.kind == AUDIO_DIAG_LEAVE && e.stage >= 0 && e.stage < AUDIO_DIAG_STAGE_COUNT) {
|
||||
auto& s = d.stages[e.stage];
|
||||
if (s.entered && s.tid == e.tid && e.wall >= s.entered) {
|
||||
s.maxWork = std::max(s.maxWork, e.wall - s.entered);
|
||||
s.maxCpu = std::max(s.maxCpu, e.cpu - s.cpu);
|
||||
}
|
||||
s.output = e.detail;
|
||||
s.entered = 0;
|
||||
} else if (e.kind >= AUDIO_DIAG_HOLE && e.kind <= AUDIO_DIAG_ERROR) {
|
||||
d.faults[e.kind] += uint64_t(std::max<int64_t>(e.value, 1));
|
||||
// Первая аномалия за окно, остальные попадут в сводку.
|
||||
if (d.faults[e.kind] == uint64_t(std::max<int64_t>(e.value, 1)))
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL,
|
||||
"AudioDiag: fault dev=%p role=%s id=%016llx kind=%s stage=%s value=%lld detail=%lld tid=%ld mono_ms=%llu",
|
||||
(void*)e.device, d.label.c_str(), (unsigned long long)d.id, kFaults[e.kind - AUDIO_DIAG_HOLE],
|
||||
e.stage >= 0 && e.stage < AUDIO_DIAG_STAGE_COUNT ? kStages[e.stage] : "none",
|
||||
(long long)e.value, (long long)e.detail, e.tid, (unsigned long long)(e.wall / 1000000));
|
||||
}
|
||||
}
|
||||
|
||||
void report(std::map<uintptr_t, Device>& devices, uint64_t wall, double elapsed) {
|
||||
const auto dropped = lost.exchange(0, std::memory_order_relaxed);
|
||||
if (dropped) DEBUG_WARN(DEBUG_CATEGORY_CALL, "AudioDiag: diagnostic events lost=%llu; interval is incomplete",
|
||||
(unsigned long long)dropped);
|
||||
std::set<long> stalledThreads;
|
||||
for (auto& [key, d] : devices) {
|
||||
for (int i = 0; i < AUDIO_DIAG_STAGE_COUNT; ++i) {
|
||||
auto& s = d.stages[i];
|
||||
if (!s.last) continue;
|
||||
const double pending = s.entered && wall >= s.entered ? (wall - s.entered) / 1e6 : 0;
|
||||
const double idle = wall >= s.last ? (wall - s.last) / 1e6 : 0;
|
||||
const auto level = s.maxWork > 50000000 || s.maxGap > 50000000 || pending > 50 || idle > 100
|
||||
? DEBUG_LEVEL_WARN : DEBUG_LEVEL_DEBUG;
|
||||
if (level == DEBUG_LEVEL_WARN) stalledThreads.insert(s.tid);
|
||||
// Без вызова backend-API: reporter никогда не трогает живые ma_device/pa_stream.
|
||||
if (debug_should_output(level, DEBUG_CATEGORY_CALL)) debug_output(level, DEBUG_CATEGORY_CALL, __func__, __FILE__, __LINE__,
|
||||
"AudioDiag: dev=%p role=%s id=%016llx stage=%s tid=%ld interval=%.3fs calls=%.1f/s frames=%.0f/s "
|
||||
"gap_max=%.2fms work_max=%.2fms cpu_max=%.2fms pending=%.2fms since_enter=%.2fms in_detail=%lld out_detail=%lld mono_ms=%llu",
|
||||
(void*)key, d.label.c_str(), (unsigned long long)d.id, kStages[i], s.tid, elapsed,
|
||||
s.calls / elapsed, s.frames / elapsed, s.maxGap / 1e6, s.maxWork / 1e6, s.maxCpu / 1e6,
|
||||
pending, idle, (long long)s.input, (long long)s.output, (unsigned long long)(wall / 1000000));
|
||||
s.calls = s.frames = s.maxGap = s.maxWork = s.maxCpu = 0;
|
||||
}
|
||||
if (d.faults[AUDIO_DIAG_HOLE] || d.faults[AUDIO_DIAG_UNFILLED] || d.faults[AUDIO_DIAG_CAPTURE_DROP] ||
|
||||
d.faults[AUDIO_DIAG_NO_PROGRESS] || d.faults[AUDIO_DIAG_BACKEND_UNDERRUN] ||
|
||||
d.faults[AUDIO_DIAG_BACKEND_OVERFLOW] || d.faults[AUDIO_DIAG_ERROR])
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "AudioDiag: dev=%p hole_frames=%llu unfilled_frames=%llu capture_drop_frames=%llu "
|
||||
"no_progress=%llu backend_underruns=%llu backend_overflows=%llu errors=%llu", (void*)key,
|
||||
(unsigned long long)d.faults[AUDIO_DIAG_HOLE], (unsigned long long)d.faults[AUDIO_DIAG_UNFILLED],
|
||||
(unsigned long long)d.faults[AUDIO_DIAG_CAPTURE_DROP],
|
||||
(unsigned long long)d.faults[AUDIO_DIAG_NO_PROGRESS],
|
||||
(unsigned long long)d.faults[AUDIO_DIAG_BACKEND_UNDERRUN],
|
||||
(unsigned long long)d.faults[AUDIO_DIAG_BACKEND_OVERFLOW], (unsigned long long)d.faults[AUDIO_DIAG_ERROR]);
|
||||
d.faults.fill(0);
|
||||
}
|
||||
for (auto tid : stalledThreads) {
|
||||
const auto path = std::string("/proc/self/task/") + std::to_string(tid) + "/";
|
||||
std::ifstream waitFile(path + "wchan"), schedFile(path + "schedstat");
|
||||
std::string wait;
|
||||
uint64_t cpu = 0, ready = 0, slices = 0;
|
||||
if (waitFile >> wait && schedFile >> cpu >> ready >> slices)
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "AudioDiag: thread tid=%ld wchan=%s cpu_total=%.2fms ready_wait_total=%.2fms slices=%llu",
|
||||
tid, wait.c_str(), cpu / 1e6, ready / 1e6, (unsigned long long)slices);
|
||||
else DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "AudioDiag: thread tid=%ld proc snapshot unavailable (thread may have exited)", tid);
|
||||
}
|
||||
}
|
||||
|
||||
void run() {
|
||||
std::map<uintptr_t, Device> devices;
|
||||
auto lastReport = now(CLOCK_MONOTONIC);
|
||||
for (;;) {
|
||||
Event e;
|
||||
for (uint64_t n = 0; n < kCapacity && take(e); ++n) consume(e, devices);
|
||||
const auto wall = now(CLOCK_MONOTONIC);
|
||||
if (wall - lastReport >= kSecond) { report(devices, wall, (wall - lastReport) / double(kSecond)); lastReport = wall; }
|
||||
if (!running.load(std::memory_order_acquire)) break;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||||
}
|
||||
report(devices, now(CLOCK_MONOTONIC), 1.0);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void audio_diag_start(void) {
|
||||
if (running.load()) return;
|
||||
writePosition = 0; readPosition = 0; lost = 0;
|
||||
for (uint64_t i = 0; i < kCapacity; ++i) queue[i].sequence.store(i, std::memory_order_relaxed);
|
||||
running.store(true, std::memory_order_release);
|
||||
try { reporter = std::thread(run); }
|
||||
catch (const std::system_error& e) {
|
||||
running = false;
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "AudioDiag: reporter start failed: %s", e.what());
|
||||
return;
|
||||
}
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "AudioDiag: started capacity=%llu reporting=1s category=call; timestamps=CLOCK_MONOTONIC",
|
||||
(unsigned long long)kCapacity);
|
||||
}
|
||||
void audio_diag_stop(void) {
|
||||
if (!running.exchange(false, std::memory_order_acq_rel)) return;
|
||||
reporter.join();
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "AudioDiag: stopped");
|
||||
}
|
||||
void audio_diag_event(const void* device, int kind, int stage, int64_t value, int64_t detail) {
|
||||
publish(device, kind, stage, value, detail, nullptr);
|
||||
}
|
||||
void audio_diag_label(const void* device, uint64_t id, const char* name) {
|
||||
publish(device, AUDIO_DIAG_LABEL, -1, int64_t(id), 0, name);
|
||||
}
|
||||
void audio_diag_log(const void* log, unsigned int level, const char* message) {
|
||||
publish(log, AUDIO_DIAG_LOG, -1, level, 0, message);
|
||||
}
|
||||
#else
|
||||
void audio_diag_start(void) {}
|
||||
void audio_diag_stop(void) {}
|
||||
void audio_diag_event(const void*, int, int, int64_t, int64_t) {}
|
||||
void audio_diag_label(const void*, uint64_t, const char*) {}
|
||||
void audio_diag_log(const void*, unsigned int, const char*) {}
|
||||
#endif
|
||||
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Только метаданные: PCM и содержимое сообщений не записываются. */
|
||||
enum audio_diag_stage {
|
||||
AUDIO_DIAG_LOOP, AUDIO_DIAG_READ, AUDIO_DIAG_WRITE, AUDIO_DIAG_MAP,
|
||||
AUDIO_DIAG_SUBMIT, AUDIO_DIAG_DUPLEX, AUDIO_DIAG_CALLBACK,
|
||||
AUDIO_DIAG_FEED, AUDIO_DIAG_PULL, AUDIO_DIAG_STAGE_COUNT
|
||||
};
|
||||
enum audio_diag_kind {
|
||||
AUDIO_DIAG_ENTER, AUDIO_DIAG_LEAVE, AUDIO_DIAG_OPEN, AUDIO_DIAG_CLOSE,
|
||||
AUDIO_DIAG_NOTIFY, AUDIO_DIAG_BACKEND_STARTED, AUDIO_DIAG_HOLE, AUDIO_DIAG_UNFILLED, AUDIO_DIAG_CAPTURE_DROP,
|
||||
AUDIO_DIAG_NO_PROGRESS, AUDIO_DIAG_BACKEND_UNDERRUN, AUDIO_DIAG_BACKEND_OVERFLOW, AUDIO_DIAG_ERROR, AUDIO_DIAG_LABEL, AUDIO_DIAG_LOG
|
||||
};
|
||||
|
||||
/* start/stop — управляющий поток; перед stop все producers должны быть остановлены. */
|
||||
void audio_diag_start(void);
|
||||
void audio_diag_stop(void);
|
||||
void audio_diag_event(const void* device, int kind, int stage, int64_t value, int64_t detail);
|
||||
void audio_diag_label(const void* device, uint64_t id, const char* name);
|
||||
void audio_diag_log(const void* log, unsigned int level, const char* message);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -1,2 +1,71 @@
|
||||
#include "audio_diagnostics.h"
|
||||
#ifdef __linux__
|
||||
#include <dlfcn.h>
|
||||
static void audio_diag_pulse_init(void* device);
|
||||
#define MA_DIAGNOSTIC_EVENT(device, kind, stage, value, detail) audio_diag_event(device, kind, stage, value, detail)
|
||||
#define MA_DIAGNOSTIC_LOG(log, level, message) audio_diag_log(log, level, message)
|
||||
#define MA_DIAGNOSTIC_DEVICE_INIT(device) audio_diag_pulse_init(device)
|
||||
#endif
|
||||
#define MINIAUDIO_IMPLEMENTATION
|
||||
#include "../../../lib/miniaudio.h"
|
||||
|
||||
#ifdef __linux__
|
||||
#ifdef MA_SUPPORT_PULSEAUDIO
|
||||
static void audio_diag_pulse_underflow(ma_pa_stream* stream, void* userdata) {
|
||||
(void)stream;
|
||||
audio_diag_event(userdata, AUDIO_DIAG_BACKEND_UNDERRUN, AUDIO_DIAG_WRITE, 1, 0);
|
||||
}
|
||||
static void audio_diag_pulse_overflow(ma_pa_stream* stream, void* userdata) {
|
||||
(void)stream;
|
||||
audio_diag_event(userdata, AUDIO_DIAG_BACKEND_OVERFLOW, AUDIO_DIAG_WRITE, 1, 0);
|
||||
}
|
||||
static void audio_diag_pulse_started(ma_pa_stream* stream, void* userdata) {
|
||||
(void)stream;
|
||||
audio_diag_event(userdata, AUDIO_DIAG_BACKEND_STARTED, AUDIO_DIAG_WRITE, 1, 0);
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Только init-поток: backend-callbacks остаются короткими наблюдателями. */
|
||||
static void audio_diag_pulse_init(void* device) {
|
||||
#ifdef MA_SUPPORT_PULSEAUDIO
|
||||
ma_device* dev = (ma_device*)device;
|
||||
ma_pa_stream_set_suspended_callback_proc underflow, overflow, started;
|
||||
if (dev->pContext->backend != ma_backend_pulseaudio || !dev->pulse.pStreamPlayback) return;
|
||||
#ifndef MA_NO_RUNTIME_LINKING
|
||||
underflow = (ma_pa_stream_set_suspended_callback_proc)dlsym(dev->pContext->pulse.pulseSO, "pa_stream_set_underflow_callback");
|
||||
overflow = (ma_pa_stream_set_suspended_callback_proc)dlsym(dev->pContext->pulse.pulseSO, "pa_stream_set_overflow_callback");
|
||||
started = (ma_pa_stream_set_suspended_callback_proc)dlsym(dev->pContext->pulse.pulseSO, "pa_stream_set_started_callback");
|
||||
#else
|
||||
underflow = pa_stream_set_underflow_callback;
|
||||
overflow = pa_stream_set_overflow_callback;
|
||||
started = pa_stream_set_started_callback;
|
||||
#endif
|
||||
if (!underflow || !overflow || !started) {
|
||||
audio_diag_log(ma_device_get_log(dev), MA_LOG_LEVEL_WARNING, "PulseAudio xrun/started callbacks unavailable");
|
||||
return;
|
||||
}
|
||||
underflow((ma_pa_stream*)dev->pulse.pStreamPlayback, audio_diag_pulse_underflow, dev);
|
||||
overflow((ma_pa_stream*)dev->pulse.pStreamPlayback, audio_diag_pulse_overflow, dev);
|
||||
started((ma_pa_stream*)dev->pulse.pStreamPlayback, audio_diag_pulse_started, dev);
|
||||
audio_diag_log(ma_device_get_log(dev), MA_LOG_LEVEL_INFO, "PulseAudio xrun/started observers registered");
|
||||
#else
|
||||
(void)device;
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Вызывается только владельцем общего контекста (GUI), вне device callbacks. */
|
||||
int audio_context_failed(ma_context* context) {
|
||||
#ifdef MA_SUPPORT_PULSEAUDIO
|
||||
if (context && context->backend == ma_backend_pulseaudio) {
|
||||
int result = ((ma_pa_mainloop_iterate_proc)context->pulse.pa_mainloop_iterate)((ma_pa_mainloop*)context->pulse.pMainLoop, 0, NULL);
|
||||
ma_pa_context_state_t state = ((ma_pa_context_get_state_proc)context->pulse.pa_context_get_state)((ma_pa_context*)context->pulse.pPulseContext);
|
||||
if (result < 0 || state == MA_PA_CONTEXT_FAILED || state == MA_PA_CONTEXT_TERMINATED) {
|
||||
ma_log_postf(ma_context_get_log(context), MA_LOG_LEVEL_ERROR, "[PulseAudio] Shared context failed iterate=%d state=%d errno=%d.\n",
|
||||
result, state, ((ma_pa_context_errno_proc)context->pulse.pa_context_errno)((ma_pa_context*)context->pulse.pPulseContext));
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
@@ -24,6 +24,8 @@ extern "C" {
|
||||
#include "chat/chat_event.h"
|
||||
#include "chat/db_sync.h"
|
||||
#include "call/call.h"
|
||||
#include "call/call_audio.h"
|
||||
#include "radio/radio_audio.h"
|
||||
#include "dm/dm_core.h"
|
||||
#include "dm/dm_mailbox.h"
|
||||
#include "gui_bridge.h"
|
||||
@@ -239,6 +241,8 @@ void UtunNode::runLoop() {
|
||||
utun_instance_destroy(m_instance); m_instance = nullptr;
|
||||
uasync_destroy(ua, 0); m_ua = nullptr; return;
|
||||
}
|
||||
call_audio_init(m_instance);
|
||||
radio_audio_init(m_instance);
|
||||
chat_core_sync_my_addresses(m_instance);
|
||||
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "chat_core + chat_sync initialized");
|
||||
@@ -252,6 +256,8 @@ void UtunNode::runLoop() {
|
||||
|
||||
if (m_instance) {
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "runLoop: destroying instance in worker thread");
|
||||
call_audio_destroy(m_instance);
|
||||
radio_audio_destroy(m_instance);
|
||||
utun_instance_destroy(m_instance);
|
||||
m_instance = nullptr;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user