From 2de9ac85a2b172922b92a0ec64dbd7cea5048c74 Mon Sep 17 00:00:00 2001 From: evgeny Date: Fri, 16 Oct 2026 16:50:44 +0200 Subject: [PATCH] Test missing microphone PCM in calls and radio without GUI stalls --- tools/chatgui/CMakeLists.txt | 2 + tools/chatgui/tests/test_audio_recovery.cpp | 67 ++++++++++++++++++++- 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/tools/chatgui/CMakeLists.txt b/tools/chatgui/CMakeLists.txt index a003f3e2..d66cecd7 100644 --- a/tools/chatgui/CMakeLists.txt +++ b/tools/chatgui/CMakeLists.txt @@ -668,6 +668,8 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Linux") "-Wl,--wrap=call_send_media") add_test(NAME test_audio_recovery COMMAND test_audio_recovery) set_tests_properties(test_audio_recovery PROPERTIES TIMEOUT 15) + add_test(NAME test_microphone_watchdog COMMAND test_audio_recovery --microphone-watchdog) + set_tests_properties(test_microphone_watchdog PROPERTIES TIMEOUT 25) add_executable(test_radio_panel tests/test_radio_panel.cpp src/radiopanel.cpp src/radio_audio_engine.cpp src/radiosettingspage.cpp src/ptt_hotkey_manager.cpp src/sound_manager.cpp src/voice_audio_io.cpp src/audio_device.cpp diff --git a/tools/chatgui/tests/test_audio_recovery.cpp b/tools/chatgui/tests/test_audio_recovery.cpp index 7c031218..486e02e2 100644 --- a/tools/chatgui/tests/test_audio_recovery.cpp +++ b/tools/chatgui/tests/test_audio_recovery.cpp @@ -11,6 +11,7 @@ #include #include #include +#include extern "C" { #include "utun_instance.h" #include "call/call_audio.h" @@ -30,6 +31,18 @@ static bool failStart, failContext, failDevice; static bool pulseIntegration; static bool latencyProbe; static unsigned probeCaptureFrames, probePlaybackFrames; +static std::mutex deviceMutex; +static std::map captureCallbacks; +static std::atomic microphoneFault{0}, captureCloses{0}; // 1: нет callback, 2: nullptr, 3: frames=0. +static std::atomic slowCaptureClose{false}; + +static void microphoneData(ma_device* device, void* out, const void* in, ma_uint32 frames) { + ma_device_data_proc callback; + { std::lock_guard guard(deviceMutex); callback = captureCallbacks.at(device); } + int fault = microphoneFault.load(); + if (fault == 1) return; + callback(device, out, fault == 2 ? nullptr : in, fault == 3 ? 0 : frames); +} extern "C" UTUN_INSTANCE* gui_bridge_get_inst() { return &instance; } extern "C" int gui_bridge_is_uasync_ready() { return instance.ua != nullptr; } @@ -58,6 +71,10 @@ extern "C" void __wrap_ma_context_uninit(ma_context* 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_device_config requested = *config; + if (config->deviceType == ma_device_type_capture) { + { std::lock_guard guard(deviceMutex); captureCallbacks[device] = config->dataCallback; } + requested.dataCallback = microphoneData; + } // Второй виртуальный вход: отдельный null device с тем же аппаратным драйвером. if (context->backend == ma_backend_null && config->deviceType == ma_device_type_capture && config->capture.pDeviceID && config->capture.pDeviceID->nullbackend == 1) requested.capture.pDeviceID = nullptr; @@ -68,7 +85,7 @@ extern "C" ma_result __wrap_ma_device_init(ma_context* context, const ma_device_ config->deviceType, requested.periodSizeInFrames, requested.sampleRate); } ma_result result = __real_ma_device_init(context, &requested, device); - if (result == MA_SUCCESS) { ++live[context]; devices.push_back(device); } + if (result == MA_SUCCESS) { std::lock_guard guard(deviceMutex); ++live[context]; devices.push_back(device); } return result; } extern "C" ma_result __wrap_ma_device_start(ma_device* device) { @@ -84,10 +101,15 @@ extern "C" ma_result __wrap_ma_device_start(ma_device* device) { } extern "C" void __wrap_ma_device_uninit(ma_device* device) { auto context = device->pContext; + bool capture = device->type == ma_device_type_capture; + if (capture && slowCaptureClose) QThread::msleep(700); __real_ma_device_uninit(device); + std::lock_guard guard(deviceMutex); REQUIRE(live[context] > 0); --live[context]; devices.erase(std::find(devices.begin(), devices.end(), device)); + captureCallbacks.erase(device); + if (capture) captureCloses++; } extern "C" int __wrap_audio_context_failed(ma_context* context) { if (failContext) { failContext = false; return 1; } @@ -249,6 +271,48 @@ static void measure_latency(SoundManager* manager, int seconds, const QString& k manager->shutdown(); REQUIRE(devices.empty()); } +/* Null backend выдаёт цифровую тишину; отсутствие PCM инъецируется независимо от уровня сигнала. */ +static void microphone_watchdog(SoundManager* manager) { + instance.ua = uasync_create(); REQUIRE(instance.ua); + REQUIRE(radio_init_instance(&instance) == 0); + REQUIRE(call_audio_init(&instance) == 0 && radio_audio_init(&instance) == 0); + auto* call = CallAudioEngine::instance(); auto* radio = RadioAudioEngine::instance(); + int callErrors = 0, beats = 0; + QObject::connect(call, &CallAudioEngine::captureErrorChanged, [&] { if (!call->captureError().isEmpty()) callErrors++; }); + QTimer heartbeat; heartbeat.setInterval(10); QObject::connect(&heartbeat, &QTimer::timeout, [&] { beats++; }); heartbeat.start(); + for (int fault : {1, 2, 3}) { + microphoneFault = 0; + REQUIRE(call->start(42) == 0 && radio->start(7) == 0); + pump(1300); // Более секунды корректной тишины не вызывает ошибку. + REQUIRE(call->captureError().isEmpty() && radio->captureError().isEmpty()); + REQUIRE(radio->captureAvailable()); radio->talkBegin(); + int before = captureStarts, closed = captureCloses, previousBeats = beats; + slowCaptureClose = true; microphoneFault = fault; + pump(1400); + REQUIRE(!call->captureError().isEmpty() && !radio->captureError().isEmpty()); + REQUIRE(call->isActive() && radio->isActive() && !radio->captureAvailable() && !radio->isTalking()); + REQUIRE(beats - previousBeats >= 80); // uninit 700мс выполняется вне GUI. + radio->talkBegin(); radio->setHotkeyTalking(true); REQUIRE(!radio->isTalking()); + pump(800); REQUIRE(captureCloses == closed + 2); + slowCaptureClose = false; microphoneFault = 0; pump(1200); + REQUIRE(captureStarts == before); // Самопроизвольный возврат PCM не перезапускает отключённый вход. + call->stop(); radio->stop(); pump(50); + REQUIRE(callErrors == fault); + } + // Первого callback нет вообще: те же ошибка и остановка, без повторных открытий. + microphoneFault = 1; + REQUIRE(call->start(43) == 0 && radio->start(8) == 0); + int before = captureStarts, closed = captureCloses; + pump(1500); + REQUIRE(!call->captureError().isEmpty() && !radio->captureError().isEmpty()); + REQUIRE(captureStarts == before && captureCloses == closed + 2); + call->stop(); radio->stop(); pump(50); + call_audio_destroy(&instance); radio_audio_destroy(&instance); radio_destroy_instance(&instance); + uasync_destroy(instance.ua, 0); instance.ua = nullptr; + manager->shutdown(); REQUIRE(devices.empty()); + qInfo("microphone watchdog: silence, no callbacks, null PCM, zero frames, no retry, nonblocking close, RX retained PASS"); +} + int main(int argc, char** argv) { QCoreApplication app(argc, argv); latencyProbe = argc >= 2 && std::strcmp(argv[1], "--latency") == 0; @@ -273,6 +337,7 @@ int main(int argc, char** argv) { } auto* manager = SoundManager::instance(); REQUIRE(manager->init() && manager->context()); + if (argc == 2 && std::strcmp(argv[1], "--microphone-watchdog") == 0) { microphone_watchdog(manager); return 0; } if (latencyProbe) { measure_latency(manager, seconds, kind); return 0; } if (pulseIntegration) { pulse_recovery(manager, QString::fromLocal8Bit(argv[2])); return 0; } ma_device_id id{};