From b483335c0630549048f54649ebc48aabd4ff562d Mon Sep 17 00:00:00 2001 From: evgeny Date: Wed, 16 Sep 2026 13:29:28 +0300 Subject: [PATCH] =?UTF-8?q?bgp/ncd:=20=D0=B2=D0=BB=D0=B0=D0=B4=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20conn=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20NCD-hand?= =?UTF-8?q?le=20=D0=B2=20senders=5Flist;=20call=20audio=20route=20persist?= =?UTF-8?q?=20+=20incoming=20call=20sound?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/routing_layer/topo_group.c | 50 +++++++++++++++++-- src/routing_layer/topo_group.h | 1 + src/routing_layer/topo_group_connect.c | 20 ++++++-- src/routing_layer/topo_group_invite.c | 20 ++++---- src/routing_layer/topo_recovery.c | 6 +-- src/transport_layer/node_conn_direct.c | 17 ++++--- .../com/utun/chat/data/CallAudioRouter.kt | 17 ++++++- .../java/com/utun/chat/data/ChatRepository.kt | 2 +- .../java/com/utun/chat/data/ConfigProvider.kt | 12 +++++ .../utun/chat/ui/screens/MemberListScreen.kt | 14 ++---- .../com/utun/chat/viewmodel/ChatViewModel.kt | 3 ++ tools/chatgui/resources/chatgui.qrc | 1 + tools/chatgui/src/mainwindow.cpp | 22 ++++++-- tools/chatgui/src/sound_manager.cpp | 34 +++++++++++++ tools/chatgui/src/sound_manager.h | 3 ++ tools/chatgui/src/soundsettingspage.cpp | 9 ++-- tools/chatgui/src/soundsettingspage.h | 2 +- 17 files changed, 185 insertions(+), 48 deletions(-) diff --git a/src/routing_layer/topo_group.c b/src/routing_layer/topo_group.c index 03b59348..8c800c6d 100644 --- a/src/routing_layer/topo_group.c +++ b/src/routing_layer/topo_group.c @@ -23,6 +23,7 @@ #include "route_connectivity.h" #include "control_server.h" #include "conn_mgr.h" +#include "../transport_layer/node_conn_direct.h" #include "topo_group_connect.h" #include "topo_recovery.h" #include "broadcast.h" @@ -324,7 +325,11 @@ static void topo_group_destroy(struct TOPO_GROUP* group) { DEBUG_INFO(DEBUG_CATEGORY_SYS, "[GRP_DESTROY] 2b broadcast_destroy done"); struct ll_entry* e; - while ((e = queue_data_get(group->senders_list)) != NULL) queue_entry_free(e); + while ((e = queue_data_get(group->senders_list)) != NULL) { + struct TOPO_GROUP_CONN_ITEM* item = (struct TOPO_GROUP_CONN_ITEM*)e; + if (item->handle) { node_conn_direct_close(item->handle); item->handle = NULL; } + queue_entry_free(e); + } queue_free(group->senders_list); DEBUG_INFO(DEBUG_CATEGORY_SYS, "[GRP_DESTROY] 3 senders_list done"); @@ -597,6 +602,24 @@ void topo_group_new_conn(struct TOPO_GROUP* group, struct ETCP_CONN* conn) { } topo_group_add_to_senders(group, conn); + /* BGP — владелец conn: держим NCD-handle (если ещё нет), чтобы соединение + * не рвалось другими владельцами (conn_mgr) при их закрытии. cb=NULL — + * события BGP уже получает через conn-status callback. */ + { struct ll_entry* se = group->senders_list->head; + while (se) { + struct TOPO_GROUP_CONN_ITEM* item = (struct TOPO_GROUP_CONN_ITEM*)se->data; + if (item->conn == conn) { + if (item->handle == NULL) { + node_conn_direct_open(conn->instance, conn->peer_node_id, NULL, NULL, &item->handle, NULL); + DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "topo_group_new_conn: BGP handle acquired node=0x%016llx h=%p", + (unsigned long long)conn->peer_node_id, (void*)item->handle); + } + break; + } + se = se->next; + } + } + DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "topo_group_new_conn: peer=%016llx group=%016llx type=%d ch=%s", (unsigned long long)conn->peer_node_id, (unsigned long long)group->group_id, group->group_type, group->channel_id); topo_group_send_table_request(group, conn); @@ -658,8 +681,24 @@ void topo_group_remove_conn(struct TOPO_GROUP* group, struct ETCP_CONN* conn) { node_entry = next; } + /* освобождаем владение BGP: сначала убираем item из senders_list (чтобы + ре-ентрантный remove_conn от conn DOWN не нашёл его), затем закрываем handle. */ e = group->senders_list->head; - while (e) { struct TOPO_GROUP_CONN_ITEM* item = (struct TOPO_GROUP_CONN_ITEM*)e->data; if (item->conn == conn) { queue_remove_data(group->senders_list, e); queue_entry_free(e); break; } e = e->next; } + while (e) { + struct TOPO_GROUP_CONN_ITEM* item = (struct TOPO_GROUP_CONN_ITEM*)e->data; + if (item->conn == conn) { + struct NODE_CONN_DIRECT* h = item->handle; + item->handle = NULL; + queue_remove_data(group->senders_list, e); + queue_entry_free(e); + if (h) { + node_conn_direct_close(h); + DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "topo_group_remove_conn: BGP handle released node=0x%016llx", (unsigned long long)conn->peer_node_id); + } + break; + } + e = e->next; + } if (cascaded > 0) topo_recovery_start(group); @@ -1019,7 +1058,12 @@ static void topo_group_add_to_senders(struct TOPO_GROUP* group, struct ETCP_CONN while (e) { if (((struct TOPO_GROUP_CONN_ITEM*)e->data)->conn == conn) { already = true; break; } e = e->next; } if (!already) { struct ll_entry* item_entry = queue_entry_new(sizeof(struct TOPO_GROUP_CONN_ITEM)); - if (item_entry) { ((struct TOPO_GROUP_CONN_ITEM*)item_entry->data)->conn = conn; queue_data_put(group->senders_list, item_entry); } + if (item_entry) { + struct TOPO_GROUP_CONN_ITEM* item = (struct TOPO_GROUP_CONN_ITEM*)item_entry->data; + item->conn = conn; + item->handle = NULL; + queue_data_put(group->senders_list, item_entry); + } } } diff --git a/src/routing_layer/topo_group.h b/src/routing_layer/topo_group.h index 51fb10e0..99b5d79c 100644 --- a/src/routing_layer/topo_group.h +++ b/src/routing_layer/topo_group.h @@ -164,6 +164,7 @@ struct TOPOMSG_ERR_GROUP_MISMATCH { struct TOPO_GROUP_CONN_ITEM { struct ll_entry ll; struct ETCP_CONN* conn; + struct NODE_CONN_DIRECT* handle; // владение BGP этим conn (NCD-handle) }; struct ETCP_LINK; diff --git a/src/routing_layer/topo_group_connect.c b/src/routing_layer/topo_group_connect.c index 8e93382a..29863d1a 100644 --- a/src/routing_layer/topo_group_connect.c +++ b/src/routing_layer/topo_group_connect.c @@ -275,6 +275,18 @@ void topo_group_connect_on_down(struct TOPO_GROUP* group, struct ETCP_CONN* conn * В Phase 2/3 — успех завершает цикл; провал закрывает handle (чтобы не оставить * залипшее состояние) и ставит паузу перед следующей попыткой. */ +/* Передача владения: после topo_group_new_conn (который взял свой NCD-handle) + * закрываем connect-фазу handle, чтобы владельцем conn остался один BGP-хэндл + * (в senders_list), без двойного счёта. */ +static void tgc_release_handle(struct TOPO_GROUP_CONNECT* gc, struct NODE_CONN_DIRECT* h) { + if (!gc || !h) return; + for (int i = 0; i < gc->handle_count; i++) { + if (gc->handles[i] == h) { gc->handles[i] = NULL; node_conn_direct_close(h); return; } + } + if (gc->cur_handle == h) { gc->cur_handle = NULL; node_conn_direct_close(h); return; } + node_conn_direct_close(h); +} + static void tgc_callback(struct NODE_CONN_DIRECT* h, enum ncd_event event, void* arg) { struct TOPO_GROUP_CONNECT* gc = (struct TOPO_GROUP_CONNECT*)arg; struct ETCP_CONN* conn = node_conn_direct_get_conn(h); @@ -283,10 +295,9 @@ static void tgc_callback(struct NODE_CONN_DIRECT* h, enum ncd_event event, void* int ok = (event == NCD_EVENT_UP); if (ok) { - struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(gc->group, node_id); - if (nq) nq->handle = h; - /* запуск BGP: узел соединён с группой */ + /* запуск BGP: узел соединён с группой (topo_group_new_conn берёт владение NCD-handle) */ if (conn) topo_group_new_conn(gc->group, conn); + tgc_release_handle(gc, h); } else if (event == NCD_EVENT_TIMEOUT) { struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(gc->group, node_id); if (nq) nq->handle = NULL; @@ -540,10 +551,9 @@ static void tgc_once_cb(struct NODE_CONN_DIRECT* h, enum ncd_event ev, void* arg struct ETCP_CONN* conn = node_conn_direct_get_conn(h); if (ev == NCD_EVENT_UP && conn) { - struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(c->group, c->node_id); - if (nq) nq->handle = h; node_conn_direct_set_callback(h, NULL, NULL); topo_group_new_conn(c->group, conn); + node_conn_direct_close(h); /* владение перешло к topo_group_new_conn */ DEBUG_INFO(DEBUG_CATEGORY_BGP, "%s: once-connect UP node=0x%016llx ch=%s", TGC_ID, (unsigned long long)c->node_id, c->group->channel_id); } else if (ev == NCD_EVENT_TIMEOUT) { diff --git a/src/routing_layer/topo_group_invite.c b/src/routing_layer/topo_group_invite.c index 51420cb9..b4d318c9 100644 --- a/src/routing_layer/topo_group_invite.c +++ b/src/routing_layer/topo_group_invite.c @@ -343,15 +343,16 @@ static void tgi_handle_info_resp(struct ETCP_CONN* conn, const uint8_t* data, si topo_group_new_conn(bgp_grp, conn); } - /* ── успех: переводим ncd-handle во владение узла, доставляем JOIN ── */ + /* ── успех: владение conn перешло к topo_group_new_conn (item->handle), + закрываем connect-фазу handle и доставляем JOIN ── */ { uint64_t node_id = inv->node_id, group_id = inv->group_id; tgi_cb_t cb = inv->cb; void* cb_arg = inv->cb_arg; - if (inv->ncd_handle) node_conn_direct_set_callback(inv->ncd_handle, NULL, NULL); - struct TOPO_GROUP* g = topo_groups_find(inv->inst->topo_groups, group_id); - struct TOPO_GROUP_NODE* nq = g ? topo_node_find_by_id(g, node_id) : NULL; - if (nq) nq->handle = inv->ncd_handle; - inv->ncd_handle = NULL; /* не закрываем — соединение переходит узлу */ + if (inv->ncd_handle) { + node_conn_direct_set_callback(inv->ncd_handle, NULL, NULL); + node_conn_direct_close(inv->ncd_handle); + inv->ncd_handle = NULL; + } inv->temp_nq = NULL; /* не удаляем узел из группы */ tgi_cancel(inv); if (cb) cb(node_id, group_id, TGI_EVENT_JOIN, cb_arg); @@ -511,10 +512,11 @@ struct tgi_conn_ctx { struct UTUN_INSTANCE* inst; uint64_t node_id, group_id; }; static void tgi_to_channel_cb(struct NODE_CONN_DIRECT* h, enum ncd_event ev, void* arg) { struct tgi_conn_ctx* c = (struct tgi_conn_ctx*)arg; if (ev == NCD_EVENT_UP) { + struct ETCP_CONN* conn = node_conn_direct_get_conn(h); struct TOPO_GROUP* g = topo_groups_find(c->inst->topo_groups, c->group_id); - struct TOPO_GROUP_NODE* nq = g ? topo_node_find_by_id(g, c->node_id) : NULL; - if (nq) nq->handle = h; - node_conn_direct_set_callback(h, NULL, NULL); + /* владение переходит к topo_group_new_conn (item->handle), connect-фазу закрываем */ + if (g && conn) topo_group_new_conn(g, conn); + node_conn_direct_close(h); u_free(c); } else if (ev == NCD_EVENT_TIMEOUT) { node_conn_direct_force_close(h); diff --git a/src/routing_layer/topo_recovery.c b/src/routing_layer/topo_recovery.c index c2c45ca2..a6f50e1e 100644 --- a/src/routing_layer/topo_recovery.c +++ b/src/routing_layer/topo_recovery.c @@ -68,9 +68,9 @@ static void topo_recovery_callback(struct NODE_CONN_DIRECT* h, enum ncd_event ev ctx->current_node_id = 0; ctx->current_handle = NULL; if (event == NCD_EVENT_UP) { - struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(ctx->group, node_id); - if (nq) nq->handle = h; - node_conn_direct_set_callback(h, NULL, NULL); + /* владение переходит к topo_group_new_conn (item->handle), connect-фазу закрываем */ + if (conn) topo_group_new_conn(ctx->group, conn); + node_conn_direct_close(h); DEBUG_INFO(DEBUG_CATEGORY_BGP, "recovery: next=%016llx reconnected to %016llx", (unsigned long long)ctx->next_hop_id, (unsigned long long)node_id); } else { node_conn_direct_close(h); diff --git a/src/transport_layer/node_conn_direct.c b/src/transport_layer/node_conn_direct.c index ee87b2c2..85f244ce 100644 --- a/src/transport_layer/node_conn_direct.c +++ b/src/transport_layer/node_conn_direct.c @@ -869,16 +869,21 @@ void node_conn_direct_close(struct NODE_CONN_DIRECT* h) { /* Удаляем handle из списка */ struct NODE_CONN_DIRECT** pp = &entry->handles; - while (*pp) { if (*pp == h) { *pp = h->next; break; } pp = &(*pp)->next; } + int found = 0; + while (*pp) { if (*pp == h) { *pp = h->next; found = 1; break; } pp = &(*pp)->next; } + if (!found) { + DEBUG_ERROR(DEBUG_CATEGORY_NCD, "[ncd] close: h=%p NOT in handles list node=0x%016llx (double-close?)", + (void*)h, (unsigned long long)node_id); + } entry->handle_count--; if (entry->handle_count < 0) { - DEBUG_ERROR(DEBUG_CATEGORY_NCD, "[ncd] FATAL: handle_count=%d < 0 node=0x%016llx", - entry->handle_count, (unsigned long long)node_id); + DEBUG_ERROR(DEBUG_CATEGORY_NCD, "[ncd] FATAL: handle_count=%d < 0 h=%p node=0x%016llx", + entry->handle_count, (void*)h, (unsigned long long)node_id); h->entry = NULL; u_free(h); return; } - DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] close node=0x%016llx remaining=%d", (unsigned long long)node_id, entry->handle_count); + DEBUG_INFO(DEBUG_CATEGORY_NCD, "[ncd] close h=%p node=0x%016llx remaining=%d", (void*)h, (unsigned long long)node_id, entry->handle_count); if (entry->handle_count == 0) { if (entry->handles) { @@ -959,8 +964,8 @@ void node_conn_direct_force_close(struct NODE_CONN_DIRECT* h) { entry->handle_count--; if (entry->handle_count < 0) { - DEBUG_ERROR(DEBUG_CATEGORY_NCD, "[ncd] FATAL: force_close handle_count=%d < 0 node=0x%016llx", - entry->handle_count, (unsigned long long)node_id); + DEBUG_ERROR(DEBUG_CATEGORY_NCD, "[ncd] FATAL: force_close handle_count=%d < 0 h=%p node=0x%016llx", + entry->handle_count, (void*)h, (unsigned long long)node_id); h->entry = NULL; u_free(h); return; } diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallAudioRouter.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallAudioRouter.kt index b94d8990..dd32aefd 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallAudioRouter.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallAudioRouter.kt @@ -35,6 +35,8 @@ class CallAudioRouter { @Volatile var route = AudioRoute.SPEAKER private set + @Volatile var preferredRoute = AudioRoute.SPEAKER + private set @Volatile var headsetAvailable = false private set @Volatile var hasEarpiece = true @@ -71,7 +73,7 @@ class CallAudioRouter { bindBtHeadsetProfile() hasEarpiece = findBuiltinEarpiece() != null - route = AudioRoute.SPEAKER + route = preferredRoute applyRoute(route) refresh() LogManager.addLog("INFO", "CallAudioRouter", "started (hasEarpiece=$hasEarpiece)") @@ -102,11 +104,18 @@ class CallAudioRouter { } if (route == r) return route = r + preferredRoute = r applyRoute(r) LogManager.addLog("INFO", "CallAudioRouter", "route -> $r (headsetAvailable=$headsetAvailable)") onChanged?.invoke(route, headsetAvailable) } + /** Инжекция сохранённого источника аудио (из DataStore) перед стартом звонка. */ + fun setPreferredRoute(r: AudioRoute) { + preferredRoute = r + LogManager.addLog("INFO", "CallAudioRouter", "preferredRoute -> $r") + } + /* ── применение маршрута ── */ private fun applyRoute(r: AudioRoute) { try { @@ -200,6 +209,12 @@ class CallAudioRouter { applyRoute(route) LogManager.addLog("INFO", "CallAudioRouter", "headset lost, fallback route -> EARPIECE") } + /* гарнитура появилась, а пользователь ранее выбрал её — восстанавливаем */ + if (now && !prev && preferredRoute == AudioRoute.HEADSET && route != AudioRoute.HEADSET) { + route = AudioRoute.HEADSET + applyRoute(route) + LogManager.addLog("INFO", "CallAudioRouter", "headset available, restore preferred route -> HEADSET") + } if (now != prev || route == AudioRoute.HEADSET) { onChanged?.invoke(route, now) } diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ChatRepository.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ChatRepository.kt index 950fff69..d47d86e8 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ChatRepository.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ChatRepository.kt @@ -24,7 +24,7 @@ data class NodeStatus( val bgpRtt: Int = 0, val bgpNodes: Int = 0 ) { - val online: Boolean get() = (directPresence && directUp) || (indirectPresence && indirectUp) + val online: Boolean get() = hasPresence val hasPresence: Boolean get() = directPresence || indirectPresence || bgpPresence } diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt index 465a37d9..30adc125 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt @@ -52,6 +52,7 @@ class ConfigProvider(private val context: Context) { private val serviceEnabledKey = booleanPreferencesKey("service_enabled") private val soundEnabledKey = booleanPreferencesKey("sound_enabled") private val mutedChannelsKey = stringSetPreferencesKey("muted_channels") + private val callAudioRouteKey = stringPreferencesKey("call_audio_route") private fun keysFilePath(): String { val dir = runBlocking { context.dataStore.data.first()[dbPath] } ?: context.filesDir.absolutePath @@ -247,6 +248,17 @@ class ConfigProvider(private val context: Context) { } } + /* ── Audio route for calls (запоминание источника аудио) ── */ + + fun getCallAudioRoute(): AudioRoute = runBlocking { + val s = context.dataStore.data.first()[callAudioRouteKey] + AudioRoute.entries.firstOrNull { it.name == s } ?: AudioRoute.SPEAKER + } + + suspend fun setCallAudioRoute(r: AudioRoute) { + context.dataStore.edit { prefs -> prefs[callAudioRouteKey] = r.name } + } + /* ── Server entries ── */ suspend fun detectInterfaces(saved: List = emptyList()): List { diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/MemberListScreen.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/MemberListScreen.kt index 49b8e0df..4ab0e075 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/MemberListScreen.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/MemberListScreen.kt @@ -141,17 +141,9 @@ private fun MemberListItem( ) { val onlineColor = when { member.connecting -> Color(0xFFFFEB3B) // yellow: connecting - member.directUp || member.indirectUp -> Color(0xFF4CAF50) // green: up - member.directPresence || member.indirectPresence || member.bgpPresence -> Color(0xFFFFA726) // orange: presence but down + member.online -> Color(0xFF4CAF50) // green: reachable (direct/indirect/relay) else -> Color(0xFF9E9E9E) // gray: offline } - val connLabel = when { - member.connecting -> "connecting" - member.directUp -> "direct" - member.indirectUp -> "indirect" - member.directPresence || member.indirectPresence || member.bgpPresence -> "offline" - else -> "" - } val bgColor = if (isSelected) MaterialTheme.colorScheme.primaryContainer else Color.Transparent Surface( @@ -213,14 +205,14 @@ private fun MemberListItem( else if (member.connecting) parts.add("connecting…") else if (member.directUp) parts.add("direct↑") else if (member.indirectUp) parts.add("indirect↑") - else if (member.directPresence || member.indirectPresence || member.bgpPresence) parts.add("down") + else if (member.online) parts.add("relay") if (member.rtt > 0) parts.add("${member.rtt}ms") if (parts.isNotEmpty()) { Row(verticalAlignment = Alignment.CenterVertically) { Text( text = parts.joinToString(" "), fontSize = 11.sp, - color = if (member.isSelf || member.directUp || member.indirectUp) + color = if (member.isSelf || member.online) Color(0xFF4CAF50) else Color.Gray ) } diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt index e70025e5..f042b0be 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt @@ -503,6 +503,8 @@ class ChatViewModel : ViewModel() { val st = _callState.value if (st != null && st.callId == callId) { LogManager.addLog("INFO", "VM", "CALL_ACCEPTED id=0x%016x".format(callId)) + callAudio.router.setPreferredRoute( + ChatApplication.instance.configProvider.getCallAudioRoute()) if (!callAudio.start(callId)) { LogManager.addLog("ERROR", "VM", "call audio engine start failed") NativeLib.callHangup(callId) @@ -629,6 +631,7 @@ class ChatViewModel : ViewModel() { fun setAudioRoute(r: AudioRoute) { callAudio.router.setRoute(r) + viewModelScope.launch { ChatApplication.instance.configProvider.setCallAudioRoute(r) } LogManager.addLog("INFO", "VM", "setAudioRoute -> $r") } diff --git a/tools/chatgui/resources/chatgui.qrc b/tools/chatgui/resources/chatgui.qrc index aaa4fa7a..7f8ffec5 100644 --- a/tools/chatgui/resources/chatgui.qrc +++ b/tools/chatgui/resources/chatgui.qrc @@ -10,6 +10,7 @@ icons/mic.png icons/send.png sounds/msg_incoming.mp3 + sounds/call_incoming.mp3 bg.jpg diff --git a/tools/chatgui/src/mainwindow.cpp b/tools/chatgui/src/mainwindow.cpp index 892e4488..eb9e0839 100644 --- a/tools/chatgui/src/mainwindow.cpp +++ b/tools/chatgui/src/mainwindow.cpp @@ -292,7 +292,7 @@ void MainWindow::setupSoundFromConfig() { SoundManager* sm = SoundManager::instance(); if (!sm->init()) return; - QString soundDir, msgInFile, msgOutFile, connectFile, disconnectFile; + QString soundDir, msgInFile, msgOutFile, connectFile, disconnectFile, callInFile; QFile cfgFile(m_cfgPath); if (cfgFile.open(QIODevice::ReadOnly | QIODevice::Text)) { bool inGui = false; @@ -307,6 +307,7 @@ void MainWindow::setupSoundFromConfig() { else if (line.startsWith("sound_msg_outgoing=")) msgOutFile = line.mid(19).trimmed(); else if (line.startsWith("sound_connect=")) connectFile = line.mid(14).trimmed(); else if (line.startsWith("sound_disconnect=")) disconnectFile = line.mid(17).trimmed(); + else if (line.startsWith("sound_call_incoming=")) callInFile = line.mid(20).trimmed(); } cfgFile.close(); } @@ -321,16 +322,21 @@ void MainWindow::setupSoundFromConfig() { else sm->loadSound("msg_incoming", resolvePath(msgInFile)); + if (callInFile.isEmpty()) + sm->loadSound("call_incoming", ":/sounds/call_incoming.mp3"); + else + sm->loadSound("call_incoming", resolvePath(callInFile)); + if (!msgOutFile.isEmpty()) sm->loadSound("msg_outgoing", resolvePath(msgOutFile)); if (!connectFile.isEmpty()) sm->loadSound("connect", resolvePath(connectFile)); if (!disconnectFile.isEmpty()) sm->loadSound("disconnect", resolvePath(disconnectFile)); if (m_db && m_db->isOpen()) { 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"}; - for (int i = 0; i < 4; i++) { + QString se[] = {"msg_incoming", "msg_outgoing", "connect", "disconnect", "call_incoming"}; + for (int i = 0; i < 5; i++) { const QString& ek = se[i]; - sm->setEvent(ek, m_db->getUiStateInt(QStringLiteral("sound_%1_on").arg(ek), i == 0 ? 1 : 0), + sm->setEvent(ek, m_db->getUiStateInt(QStringLiteral("sound_%1_on").arg(ek), (i == 0 || i == 4) ? 1 : 0), m_db->getUiStateInt(QStringLiteral("sound_%1_vol").arg(ek), 80) / 100.0f); } } @@ -528,6 +534,12 @@ void MainWindow::onCallIncoming(const QString& chId, quint64 caller, quint64 cal QString name = m_db->getDisplayName(caller); if (name.isEmpty()) name = QStringLiteral("0x%1").arg(caller, 16, 16, QChar('0')); + SoundManager::instance()->playLoop("call_incoming"); + if (!isVisible() || isMinimized()) { + m_trayIcon->showMessage("vibechat", QStringLiteral("Incoming call from %1").arg(name), + QSystemTrayIcon::Information, 5000); + } + CallWindow* w = new CallWindow(this); w->setCallId(callId); w->setPeerName(name); @@ -545,6 +557,7 @@ void MainWindow::onCallRinging(quint64 callId) { } void MainWindow::onCallAccepted(quint64 callId) { + SoundManager::instance()->stopLoop(); CallWindow* w = m_activeCalls.value(callId, nullptr); if (w) w->setState(CallWindow::Active); if (m_db) @@ -561,6 +574,7 @@ void MainWindow::onCallEnded(quint64 callId, uint8_t reason) { } void MainWindow::closeCallWindow(quint64 callId) { + SoundManager::instance()->stopLoop(); CallWindow* w = m_activeCalls.take(callId); if (!w) return; w->disconnect(this); diff --git a/tools/chatgui/src/sound_manager.cpp b/tools/chatgui/src/sound_manager.cpp index cb6c14f0..eb5cb93d 100644 --- a/tools/chatgui/src/sound_manager.cpp +++ b/tools/chatgui/src/sound_manager.cpp @@ -56,6 +56,7 @@ void SoundManager::shutdown() { 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; + m_loopName.clear(); } void SoundManager::setEnabled(bool enabled) { m_enabled = enabled; } @@ -151,6 +152,39 @@ void SoundManager::play(const QString& name) { ma_sound_start(s); } +void SoundManager::playLoop(const QString& name) { + if (!m_enabled) return; + if (!m_initialized) { DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "SoundManager::playLoop: not initialized"); return; } + + EventSettings es = m_eventSettings.value(name); + if (!es.enabled) return; + + auto it = m_sounds.find(name); + if (it == m_sounds.end()) { + DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "SoundManager::playLoop: sound '%s' not loaded", qPrintable(name)); + return; + } + + ma_sound* s = it->sound; + ma_sound_stop(s); + ma_sound_seek_to_pcm_frame(s, 0); + ma_sound_set_volume(s, m_volume * es.volume); + ma_sound_set_looping(s, MA_TRUE); + ma_sound_start(s); + m_loopName = name; +} + +void SoundManager::stopLoop() { + if (m_loopName.isEmpty()) return; + auto it = m_sounds.find(m_loopName); + m_loopName.clear(); + if (it == m_sounds.end()) return; + ma_sound* s = it->sound; + ma_sound_stop(s); + ma_sound_set_looping(s, MA_FALSE); + ma_sound_seek_to_pcm_frame(s, 0); +} + ma_context* SoundManager::context() { return m_context; } diff --git a/tools/chatgui/src/sound_manager.h b/tools/chatgui/src/sound_manager.h index c7928ee7..6e0f5b56 100644 --- a/tools/chatgui/src/sound_manager.h +++ b/tools/chatgui/src/sound_manager.h @@ -35,6 +35,8 @@ public: bool loadSound(const QString& name, const QString& filePath); void play(const QString& name); + void playLoop(const QString& name); + void stopLoop(); ma_context* context(); @@ -76,4 +78,5 @@ private: float m_volume = 0.8f; QHash m_eventSettings; QHash m_sounds; + QString m_loopName; }; diff --git a/tools/chatgui/src/soundsettingspage.cpp b/tools/chatgui/src/soundsettingspage.cpp index d2d93c1c..b6e9e96a 100644 --- a/tools/chatgui/src/soundsettingspage.cpp +++ b/tools/chatgui/src/soundsettingspage.cpp @@ -59,8 +59,9 @@ SoundSettingsPage::SoundSettingsPage(DbManager* db, QWidget* parent) m_events[1] = {"msg_outgoing", "Outgoing message", nullptr, nullptr, nullptr}; m_events[2] = {"connect", "Peer connected", nullptr, nullptr, nullptr}; m_events[3] = {"disconnect", "Peer disconnected", nullptr, nullptr, nullptr}; + m_events[4] = {"call_incoming", "Incoming call", nullptr, nullptr, nullptr}; - for (int i = 0; i < 4; i++) { + for (int i = 0; i < 5; i++) { auto& e = m_events[i]; auto* row = new QHBoxLayout(); row->setSpacing(8); @@ -118,11 +119,11 @@ void SoundSettingsPage::loadFromDb() { m_volumeLabel->setEnabled(enabled); m_volumeLabel->setText(QString::number(vol) + "%"); - for (int i = 0; i < 4; i++) { + for (int i = 0; i < 5; i++) { auto& e = m_events[i]; QString onKey = QStringLiteral("sound_%1_on").arg(e.key); QString volKey = QStringLiteral("sound_%1_vol").arg(e.key); - bool on = m_db->getUiStateInt(onKey, i == 0 ? 1 : 0); // only msg_incoming on by default + bool on = m_db->getUiStateInt(onKey, (i == 0 || i == 4) ? 1 : 0); // msg_incoming и call_incoming on by default int ev = m_db->getUiStateInt(volKey, 80); e.check->setChecked(on); e.slider->setValue(ev); @@ -138,7 +139,7 @@ void SoundSettingsPage::applyAndSave() { sm->setEnabled(enabled); sm->setVolume(masterVol); - for (int i = 0; i < 4; i++) { + for (int i = 0; i < 5; i++) { auto& e = m_events[i]; bool on = e.check->isChecked(); float ev = e.slider->value() / 100.0f; diff --git a/tools/chatgui/src/soundsettingspage.h b/tools/chatgui/src/soundsettingspage.h index bf1e0fa1..9e09c59d 100644 --- a/tools/chatgui/src/soundsettingspage.h +++ b/tools/chatgui/src/soundsettingspage.h @@ -28,5 +28,5 @@ private: QCheckBox* m_enabledCheck; QSlider* m_volumeSlider; QLabel* m_volumeLabel; - EventRow m_events[4]; + EventRow m_events[5]; };