diff --git a/tools/chatgui/CMakeLists.txt b/tools/chatgui/CMakeLists.txt index 50d0d235..2a33c3b3 100644 --- a/tools/chatgui/CMakeLists.txt +++ b/tools/chatgui/CMakeLists.txt @@ -677,6 +677,8 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Linux") 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_test(NAME test_audio_buffer_tuning COMMAND test_audio_recovery --buffer-tuning) + set_tests_properties(test_audio_buffer_tuning PROPERTIES TIMEOUT 20) 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 1a1824d8..e4d9d243 100644 --- a/tools/chatgui/tests/test_audio_recovery.cpp +++ b/tools/chatgui/tests/test_audio_recovery.cpp @@ -321,6 +321,80 @@ static void microphone_watchdog(SoundManager* manager) { qInfo("microphone watchdog: silence, no callbacks, null PCM, zero frames, no retry, nonblocking close, RX retained PASS"); } +/* Управляемое время проверяет длительные интервалы и предел без минут ожидания. */ +static void buffer_tuning_policy() { + AudioBufferTuning tuning; + tuning.begin(0, 10, 3); + REQUIRE(tuning.observe(500, 100) == AudioBufferTuning::Unchanged && !tuning.requestedMs); + REQUIRE(tuning.observe(1500, 100) == AudioBufferTuning::Unchanged); // Одна пачка — один сбой. + REQUIRE(tuning.observe(7000, 1) == AudioBufferTuning::Unchanged); // Старый единичный сбой уже забыт. + REQUIRE(tuning.observe(7200, 1) == AudioBufferTuning::Increase && tuning.requestedMs == 15); + tuning.begin(7300, 15, 3); + REQUIRE(tuning.observe(7500, 100) == AudioBufferTuning::Unchanged); + REQUIRE(tuning.observe(8500, 1) == AudioBufferTuning::Unchanged); + REQUIRE(tuning.observe(8700, 1) == AudioBufferTuning::Unchanged && tuning.requestedMs == 15); + REQUIRE(tuning.observe(12200, 0) == AudioBufferTuning::Unchanged); // Один лишь таймер не запускает reopen. + REQUIRE(tuning.observe(12200, 1) == AudioBufferTuning::Increase && tuning.requestedMs == 20); + int64_t now = 12200; + for (unsigned expected : {25u, 30u, 33u}) { + tuning.begin(now + 100, tuning.requestedMs, 3); now += 5000; + REQUIRE(tuning.observe(now, 1) == AudioBufferTuning::Unchanged); + REQUIRE(tuning.observe(now + 200, 1) == AudioBufferTuning::Increase && tuning.requestedMs == expected); + REQUIRE(tuning.requestedMs * tuning.periods <= 100); now += 200; + } + tuning.begin(now + 100, 33, 3); now += 5000; + REQUIRE(tuning.observe(now, 1) == AudioBufferTuning::Unchanged); + REQUIRE(tuning.observe(now + 200, 1) == AudioBufferTuning::Limit && tuning.requestedMs == 33); + ma_context context{}; context.backend = ma_backend_pulseaudio; + auto config = ma_device_config_init(ma_device_type_capture); config.periodSizeInMilliseconds = 20; + AudioDevice::requestLowLatency(&context, config); + REQUIRE(config.periodSizeInMilliseconds == 10 && !config.periodSizeInFrames); +} + +/* Та же notification, что у WASAPI/ALSA/PA; устройства и Qt recovery настоящие, backend виртуальный. */ +static void inject_capture_glitch() { + std::lock_guard guard(deviceMutex); + for (auto* device : devices) if (device->type == ma_device_type_capture) { + ma_device_notification event{}; event.pDevice = device; event.type = ma_device_notification_type_capture_overrun; + device->onNotification(&event); + } +} + +static void adaptive_buffers(SoundManager* manager) { + buffer_tuning_policy(); + 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(); + REQUIRE(call->start(70) == 0 && radio->start(71) == 0); + pump(1300); + std::vector initial; + for (auto* device : devices) if (device->type == ma_device_type_capture) initial.push_back(device->capture.internalPeriodSizeInFrames); + std::sort(initial.begin(), initial.end()); REQUIRE(initial.size() == 2); + int before = captureStarts; + inject_capture_glitch(); pump(250); REQUIRE(captureStarts == before); + inject_capture_glitch(); pump(600); REQUIRE(captureStarts == before + 2); + std::vector grown; + for (auto* device : devices) if (device->type == ma_device_type_capture) grown.push_back(device->capture.internalPeriodSizeInFrames); + std::sort(grown.begin(), grown.end()); REQUIRE(grown.size() == 2); + REQUIRE(grown[0] == initial[0] + 240 && grown[1] == initial[1] + 240); + REQUIRE(call->captureError().isEmpty() && radio->captureError().isEmpty() && radio->captureAvailable()); + pump(4500); slowCaptureClose = true; + inject_capture_glitch(); pump(250); inject_capture_glitch(); pump(250); + QElapsedTimer elapsed; elapsed.start(); call->stop(); radio->stop(); + REQUIRE(elapsed.elapsed() < 300); // Stop не ждёт выполняющееся закрытие для изменения буфера. + pump(900); slowCaptureClose = false; + REQUIRE(call->start(72) == 0 && radio->start(73) == 0); pump(100); + std::vector reset; + for (auto* device : devices) if (device->type == ma_device_type_capture) reset.push_back(device->capture.internalPeriodSizeInFrames); + std::sort(reset.begin(), reset.end()); REQUIRE(reset == initial); + 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("adaptive buffers: startup, isolated faults, cooldown, limit, actual reopen, async stop, new session baseline PASS"); +} + int main(int argc, char** argv) { QCoreApplication app(argc, argv); latencyProbe = argc >= 2 && std::strcmp(argv[1], "--latency") == 0; @@ -346,6 +420,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 (argc == 2 && std::strcmp(argv[1], "--buffer-tuning") == 0) { adaptive_buffers(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{};