Browse Source

Test missing microphone PCM in calls and radio without GUI stalls

master
evgeny 2 days ago
parent
commit
2de9ac85a2
  1. 2
      tools/chatgui/CMakeLists.txt
  2. 67
      tools/chatgui/tests/test_audio_recovery.cpp

2
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

67
tools/chatgui/tests/test_audio_recovery.cpp

@ -11,6 +11,7 @@
#include <algorithm>
#include <map>
#include <vector>
#include <mutex>
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<ma_device*, ma_device_data_proc> captureCallbacks;
static std::atomic<int> microphoneFault{0}, captureCloses{0}; // 1: нет callback, 2: nullptr, 3: frames=0.
static std::atomic<bool> 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{};

Loading…
Cancel
Save