/* Настоящее ядро, SQLite и Opus: локальное отключение источника, события и восстановление после restart чата. */ #include #include #include #include #include #include extern "C" { #include "utun_instance.h" #include "config_parser.h" #include "chat/chat_core.h" #include "chat/chat_member.h" #include "chat/chat_event.h" #include "routing_layer/topo_group.h" #include "routing_layer/topo_node_sqlite.h" #include "radio/radio.h" #include "radio/radio_audio.h" #include "u_async.h" #include "debug_config.h" #include "opus_codec.h" #include "mem.h" #include "silero_vad.h" } #ifdef UTUN_TEST_WRAP_VAD /* Детерминированная речь; автомат VAD, кодер и признак занятого эфира остаются настоящими. */ extern "C" silero_vad_t* __wrap_silero_vad_create_default() { return reinterpret_cast(1); } extern "C" void __wrap_silero_vad_destroy(silero_vad_t*) {} extern "C" void __wrap_silero_vad_reset(silero_vad_t*) {} extern "C" int __wrap_silero_vad_process(silero_vad_t*, const float*, float* probability) { *probability = 1.0f; return 0; } #endif #define REQUIRE(condition) do { if (!(condition)) { std::fprintf(stderr, "radio mute line %d: %s\n", __LINE__, #condition); std::abort(); } } while (0) static constexpr uint64_t SOURCE_A = 42, SOURCE_B = 43; static int updates, results, successes; static uint8_t last_flags; static void event(UTUN_INSTANCE*, int type, const uint8_t* data, int len) { if (type == CHAT_EVT_MEMBER_UPDATED && len == data[0] + 2 + CHAT_MEMBER_DISPLAY_SIZE) { chat_member_display m{}; REQUIRE(chat_core_deserialize_member(data + data[0] + 2, &m) == 0); if (m.node_id == SOURCE_A) { ++updates; last_flags = m.flags; } } if (type == CHAT_EVT_RADIO_MUTE_RESULT) { REQUIRE(len == 17); ++results; successes += data[16] != 0; } } static TOPO_GROUP* first_channel(UTUN_INSTANCE* inst) { for (ll_entry* e = inst->topo_groups->group_list->head; e; e = e->next) { auto* group = reinterpret_cast(e); if (group->group_type == TOPO_GROUP_TYPE_CHAT) return group; } return nullptr; } static void frames(UTUN_INSTANCE* inst, uint64_t group, uint64_t source, uint16_t first_seq = 0) { auto* enc = opus_codec_encoder_create(48000, 1); REQUIRE(enc); int16_t pcm[960]; for (int i = 0; i < 960; ++i) pcm[i] = 7000; for (uint16_t i = 0; i < 8; ++i) { uint8_t packet[256]; int bytes = opus_codec_encode(enc, pcm, 960, packet, sizeof(packet)); REQUIRE(bytes > 0); radio_audio_on_frame(inst, group, source, 1, first_seq + i, 0, packet, bytes, nullptr); } opus_codec_encoder_destroy(enc); } static std::vector pull(uint64_t group) { std::vector pcm(16 * 1920); for (int i = 0; i < 16; ++i) REQUIRE(radio_audio_pull_pcm(group, pcm.data() + i * 1920, 1920) == 1920); return pcm; } static bool audible(const std::vector& pcm) { for (int16_t sample : pcm) if (sample > 1000 || sample < -1000) return true; return false; } static bool flag(UTUN_INSTANCE* inst, const std::string& channel, uint64_t source) { uint8_t member[CHAT_MEMBER_DISPLAY_SIZE]; REQUIRE(chat_core_get_single_member(inst, channel.c_str(), source, member) == 0); uint8_t* members = nullptr; int count = 0; REQUIRE(chat_core_get_member_list(inst, channel.c_str(), &members, &count) == 0); bool found = false; for (int i = 0; i < count; ++i) { chat_member_display m{}; REQUIRE(chat_core_deserialize_member(members + i * CHAT_MEMBER_DISPLAY_SIZE, &m) == 0); if (m.node_id == source) { REQUIRE(m.flags == member[10]); found = true; } } u_free(members); REQUIRE(found); return member[10] & CHAT_MEMBER_FLAG_RADIO_MUTED; } int main() { debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); debug_set_category_level_by_name("radio", "debug"); UASYNC* ua = uasync_create(); REQUIRE(ua); QTemporaryDir directory; REQUIRE(directory.isValid()); std::string config = "[global]\nmy_private_key=704f2e012c8fa8768130cb0f988a997dccb628372bc5ceccacc78dcbfec5916f\n" "my_public_key=b3193173def895bd0fcea6f86af077c7d77216f10395275f627ac18242ec0f01\n" "db_path=" + directory.path().toStdString() + "\n[chatserver]\ngroup_autoconnect=0\nstorage_autoload=0\n"; UTUN_INSTANCE* inst = utun_instance_create_from_str(ua, config.c_str()); REQUIRE(inst); inst->config->global.db_sync_enabled = 0; REQUIRE(utun_core_start(inst) == 0 && chat_service_start(inst) == 0); chat_core_create_channel_auto(inst, "mute test"); TOPO_GROUP* group = first_channel(inst); REQUIRE(group); uint64_t gid = group->group_id; std::string channel = group->channel_id; uint8_t key[32] = {1}; REQUIRE(topo_node_sqlite_member_placeholder_put(inst->topo_sqlite_db, channel.c_str(), SOURCE_A, key, key) == 0); REQUIRE(topo_node_sqlite_member_placeholder_put(inst->topo_sqlite_db, channel.c_str(), SOURCE_B, key, key) == 0); chat_event_set_handler(inst, event); REQUIRE(radio_audio_init(inst) == 0); radio_set_active(inst, gid, 1); REQUIRE(radio_audio_start(gid, 0) == 0); frames(inst, gid, SOURCE_B); auto reference = pull(gid); REQUIRE(audible(reference)); radio_audio_stop(); REQUIRE(radio_audio_start(gid, 0) == 0); /* Отключение посреди передачи удаляет уже накопленный A, сохраняя точный PCM источника B. */ frames(inst, gid, SOURCE_A); frames(inst, gid, SOURCE_B); REQUIRE(chat_core_toggle_member_radio_mute(inst, channel.c_str(), SOURCE_A) == 0); chat_core_create_channel_auto(inst, "independent mute"); TOPO_GROUP* other = nullptr; for (ll_entry* e = inst->topo_groups->group_list->head; e; e = e->next) { auto* candidate = reinterpret_cast(e); if (candidate->group_id != gid && candidate->group_type == TOPO_GROUP_TYPE_CHAT) other = candidate; } REQUIRE(other); REQUIRE(topo_node_sqlite_member_placeholder_put(inst->topo_sqlite_db, other->channel_id, SOURCE_A, key, key) == 0); radio_set_active(inst, other->group_id, 1); REQUIRE(!flag(inst, other->channel_id, SOURCE_A) && !radio_member_muted(inst, other->group_id, SOURCE_A)); REQUIRE(flag(inst, channel, SOURCE_A) && radio_member_muted(inst, gid, SOURCE_A)); REQUIRE(last_flags & CHAT_MEMBER_FLAG_RADIO_MUTED); REQUIRE(pull(gid) == reference); frames(inst, gid, SOURCE_A, 8); REQUIRE(!audible(pull(gid))); REQUIRE(chat_core_toggle_member_radio_mute(inst, channel.c_str(), SOURCE_A) == 0); frames(inst, gid, SOURCE_A, 16); REQUIRE(audible(pull(gid))); REQUIRE(!flag(inst, channel, SOURCE_A) && !radio_member_muted(inst, gid, SOURCE_A)); /* Два подряд нажатия обрабатываются по актуальному состоянию, каждое получает результат. */ for (int i = 0; i < 2; ++i) { auto* req = static_cast(u_calloc(1, sizeof(chat_member_radio_mute_req))); REQUIRE(req); req->inst = inst; req->node_id = SOURCE_A; std::strcpy(req->ch_id, channel.c_str()); chat_core_toggle_member_radio_mute_trampoline(req); } REQUIRE(results == 2 && successes == 2 && updates == 4 && !flag(inst, channel, SOURCE_A)); REQUIRE(chat_core_toggle_member_radio_mute(inst, channel.c_str(), inst->node_id) < 0); REQUIRE(chat_core_toggle_member_radio_mute(inst, channel.c_str(), 999) < 0); REQUIRE(radio_set_member_muted(inst, gid, SOURCE_A, 2) < 0); REQUIRE(sqlite3_exec(inst->topo_sqlite_db, "PRAGMA query_only=ON", nullptr, nullptr, nullptr) == SQLITE_OK); auto* rejected = static_cast(u_calloc(1, sizeof(chat_member_radio_mute_req))); REQUIRE(rejected); rejected->inst = inst; rejected->node_id = SOURCE_A; std::strcpy(rejected->ch_id, channel.c_str()); chat_core_toggle_member_radio_mute_trampoline(rejected); REQUIRE(results == 3 && successes == 2 && updates == 4); REQUIRE(!flag(inst, channel, SOURCE_A) && !radio_member_muted(inst, gid, SOURCE_A)); REQUIRE(sqlite3_exec(inst->topo_sqlite_db, "PRAGMA query_only=OFF", nullptr, nullptr, nullptr) == SQLITE_OK); /* Локальные поля сохраняются при замене подписанного блока участника. */ REQUIRE(chat_core_toggle_member_radio_mute(inst, channel.c_str(), SOURCE_A) == 0); uint8_t signature[64] = {1}; REQUIRE(topo_node_sqlite_member_block_put(inst->topo_sqlite_db, channel.c_str(), SOURCE_A, signature, 1, signature, 2, key, key, "{}", nullptr, 0, signature) == 0); REQUIRE(flag(inst, channel, SOURCE_A)); /* Возвращаем placeholder: тест restart не требует поддельной криптоподписи. */ std::string sql = "UPDATE \"peers_" + channel + "\" SET source=1 WHERE node_id=42"; REQUIRE(sqlite3_exec(inst->topo_sqlite_db, sql.c_str(), nullptr, nullptr, nullptr) == SQLITE_OK); radio_audio_stop(); radio_audio_destroy(inst); chat_service_stop(inst); REQUIRE(chat_service_start(inst) == 0); REQUIRE(flag(inst, channel, SOURCE_A)); radio_set_active(inst, gid, 1); REQUIRE(radio_member_muted(inst, gid, SOURCE_A)); REQUIRE(radio_audio_init(inst) == 0 && radio_audio_start(gid, 0) == 0); frames(inst, gid, SOURCE_A); REQUIRE(!audible(pull(gid))); frames(inst, gid, SOURCE_B); REQUIRE(audible(pull(gid))); #ifdef UTUN_TEST_WRAP_VAD radio_audio_stop(); REQUIRE(radio_audio_start(gid, 0) == 0 && radio_audio_capture_start(gid, 1, 1) == 0); REQUIRE(radio_audio_vad_mode()); int16_t speech[1920]; for (int16_t& sample : speech) sample = 7000; frames(inst, gid, SOURCE_A); REQUIRE(!audible(pull(gid))); for (int i = 0; i < 6; ++i) REQUIRE(radio_audio_feed_pcm(gid, speech, 1920) == 0); REQUIRE(!radio_audio_transmitting()); /* Заглушённый голос удерживает авто-PTT. */ radio_audio_stop(); REQUIRE(radio_audio_start(gid, 0) == 0 && radio_audio_capture_start(gid, 1, 1) == 0); for (int i = 0; i < 6; ++i) REQUIRE(radio_audio_feed_pcm(gid, speech, 1920) == 0); REQUIRE(radio_audio_transmitting()); /* При свободном эфире та же речь запускает передачу. */ #endif radio_audio_stop(); radio_audio_destroy(inst); utun_instance_destroy(inst); inst = utun_instance_create_from_str(ua, config.c_str()); REQUIRE(inst); inst->config->global.db_sync_enabled = 0; REQUIRE(utun_core_start(inst) == 0 && chat_service_start(inst) == 0 && flag(inst, channel, SOURCE_A)); radio_set_active(inst, gid, 1); REQUIRE(radio_member_muted(inst, gid, SOURCE_A)); REQUIRE(radio_audio_init(inst) == 0 && radio_audio_start(gid, 0) == 0); frames(inst, gid, SOURCE_A); REQUIRE(!audible(pull(gid))); frames(inst, gid, SOURCE_B); REQUIRE(audible(pull(gid))); radio_audio_stop(); radio_audio_destroy(inst); sql = "UPDATE \"peers_" + channel + "\" SET local_nick='local alias' WHERE node_id=42"; REQUIRE(sqlite3_exec(inst->topo_sqlite_db, sql.c_str(), nullptr, nullptr, nullptr) == SQLITE_OK); sql = "ALTER TABLE \"peers_" + channel + "\" DROP COLUMN radio_muted"; REQUIRE(sqlite3_exec(inst->topo_sqlite_db, sql.c_str(), nullptr, nullptr, nullptr) == SQLITE_OK); utun_instance_destroy(inst); inst = utun_instance_create_from_str(ua, config.c_str()); REQUIRE(inst); inst->config->global.db_sync_enabled = 0; REQUIRE(utun_core_start(inst) == 0 && chat_service_start(inst) == 0 && !flag(inst, channel, SOURCE_A)); uint8_t member[CHAT_MEMBER_DISPLAY_SIZE]; REQUIRE(chat_core_get_single_member(inst, channel.c_str(), SOURCE_A, member) == 0); REQUIRE(!std::strcmp(reinterpret_cast(member + 15), "local alias")); utun_instance_destroy(inst); uasync_destroy(ua, 0); std::puts("PASS: audio mute/unmute, other source PCM, events, DB errors, channel isolation, VAD busy, member update, restart, schema upgrade"); return 0; }