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

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

No files matched your search

@@ -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);
+25 -1
View File
@@ -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()
+96
View File
@@ -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; }
+38
View File
@@ -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;
};
+35 -20
View File
@@ -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();
+2 -2
View File
@@ -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;
+78 -116
View File
@@ -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);
}
+17 -15
View File
@@ -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);
};
+136 -204
View File
@@ -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();
}
+17 -24
View File
@@ -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;
};
+6 -6
View File
@@ -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) {
+9 -8
View File
@@ -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);
}
+119 -178
View File
@@ -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;
}
}
+11 -15
View File
@@ -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мс) */
};
+130 -18
View File
@@ -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;
}
+24 -1
View File
@@ -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;
+95
View File
@@ -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");
}
+222
View File
@@ -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;
}
+17
View File
@@ -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;
}
+163
View File
@@ -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
+69
View File
@@ -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;
}
+6
View File
@@ -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;
}