Test desktop VAD panel through Qt buttons and real audio workers
This commit is contained in:
1 parent
8820e9a3c9
commit
312c2dd386
2 files changed
+191
No files matched your search
@@ -313,6 +313,17 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
"-Wl,--wrap=ma_device_init" "-Wl,--wrap=ma_device_start" "-Wl,--wrap=ma_device_uninit" "-Wl,--wrap=audio_context_failed")
|
||||
add_test(NAME test_audio_recovery COMMAND test_audio_recovery)
|
||||
set_tests_properties(test_audio_recovery PROPERTIES TIMEOUT 15)
|
||||
add_executable(test_radio_panel tests/test_radio_panel.cpp src/radiopanel.cpp src/radio_audio_engine.cpp
|
||||
src/sound_manager.cpp src/voice_audio_io.cpp src/audio_device.cpp
|
||||
transport/miniaudio_impl.c transport/audio_diagnostics.cpp transport/config_updater.cpp)
|
||||
target_include_directories(test_radio_panel PRIVATE ${UTUN_INCLUDE_DIRS})
|
||||
target_link_libraries(test_radio_panel PRIVATE ${QT_LIBS} ${QT_TEST_LIB} utun_voice utun pthread dl)
|
||||
target_link_options(test_radio_panel PRIVATE "-Wl,--wrap=ma_context_init" "-Wl,--wrap=ma_device_start"
|
||||
"-Wl,--wrap=radio_talk_begin" "-Wl,--wrap=radio_talk_end" "-Wl,--wrap=radio_talk_send"
|
||||
"-Wl,--wrap=silero_vad_create_default" "-Wl,--wrap=silero_vad_destroy" "-Wl,--wrap=silero_vad_reset"
|
||||
"-Wl,--wrap=silero_vad_process")
|
||||
add_test(NAME test_radio_panel COMMAND test_radio_panel)
|
||||
set_tests_properties(test_radio_panel PROPERTIES TIMEOUT 15 ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
|
||||
add_executable(test_ptt_portal tests/test_ptt_portal.cpp src/ptt_portal.cpp)
|
||||
target_link_libraries(test_ptt_portal PRIVATE ${QT_DBUS} ${QT_TEST_LIB} utun pthread)
|
||||
find_program(DBUS_RUN_SESSION dbus-run-session)
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
/* Настоящие Qt-кнопки, null audio backend и общий radio_audio; подменяем только вероятность VAD и сетевой выход. */
|
||||
#include "../src/radiopanel.h"
|
||||
#include "../src/radio_audio_engine.h"
|
||||
#include "../src/sound_manager.h"
|
||||
#include <QApplication>
|
||||
#include <QPushButton>
|
||||
#include <QLabel>
|
||||
#include <QToolButton>
|
||||
#include <QMenu>
|
||||
#include <QTemporaryDir>
|
||||
#include <QFile>
|
||||
#include <QElapsedTimer>
|
||||
#include <QTest>
|
||||
#include <atomic>
|
||||
extern "C" {
|
||||
#include "utun_instance.h"
|
||||
#include "radio/radio_audio.h"
|
||||
#include "radio/radio.h"
|
||||
#include "chat/chat_setting.h"
|
||||
#include "u_async.h"
|
||||
#include "debug_config.h"
|
||||
#include "silero_vad.h"
|
||||
}
|
||||
|
||||
#define REQUIRE(condition) do { if (!(condition)) qFatal("radio panel line %d: %s", __LINE__, #condition); } while (0)
|
||||
static UTUN_INSTANCE instance{};
|
||||
static std::atomic<float> probability{0};
|
||||
static bool detectorAvailable = true;
|
||||
static int begins, ends, packets, inputs, outputs;
|
||||
|
||||
extern "C" UTUN_INSTANCE* gui_bridge_get_inst() { return &instance; }
|
||||
extern "C" int gui_bridge_is_uasync_ready() { return instance.ua != nullptr; }
|
||||
extern "C" void gui_bridge_post_uasync_fn(void (*fn)(void*), void* arg) { uasync_post(instance.ua, fn, arg); }
|
||||
extern "C" int __wrap_radio_talk_begin(UTUN_INSTANCE*, uint64_t) { ++begins; return 0; }
|
||||
extern "C" int __wrap_radio_talk_end(UTUN_INSTANCE*, uint64_t) { ++ends; return 0; }
|
||||
extern "C" int __wrap_radio_talk_send(UTUN_INSTANCE*, uint64_t, const uint8_t*, int) { ++packets; return 0; }
|
||||
extern "C" silero_vad_t* __wrap_silero_vad_create_default() { return detectorAvailable ? reinterpret_cast<silero_vad_t*>(1) : nullptr; }
|
||||
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* result) { *result = probability.load(); return 0; }
|
||||
extern "C" ma_result __real_ma_context_init(const ma_backend*, ma_uint32, const ma_context_config*, ma_context*);
|
||||
extern "C" ma_result __real_ma_device_start(ma_device*);
|
||||
extern "C" ma_result __wrap_ma_context_init(const ma_backend*, ma_uint32, const ma_context_config*, ma_context* context) {
|
||||
ma_backend backend = ma_backend_null;
|
||||
return __real_ma_context_init(&backend, 1, nullptr, context);
|
||||
}
|
||||
extern "C" ma_result __wrap_ma_device_start(ma_device* device) {
|
||||
if (device->type == ma_device_type_capture) ++inputs; else ++outputs;
|
||||
return __real_ma_device_start(device);
|
||||
}
|
||||
|
||||
static void pump(int milliseconds) {
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
while (timer.elapsed() < milliseconds) {
|
||||
uasync_poll(instance.ua, 10);
|
||||
QApplication::processEvents();
|
||||
}
|
||||
}
|
||||
|
||||
template<class Predicate> static void await(Predicate ready) {
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
while (!ready() && timer.elapsed() < 2500) pump(10);
|
||||
REQUIRE(ready());
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
QApplication app(argc, argv);
|
||||
debug_config_init();
|
||||
debug_set_level(DEBUG_LEVEL_WARN);
|
||||
debug_set_category_level(DEBUG_CATEGORY_RADIO, DEBUG_LEVEL_INFO);
|
||||
instance.ua = uasync_create();
|
||||
REQUIRE(instance.ua && radio_audio_init(&instance) == 0);
|
||||
REQUIRE(chat_setting_set(&instance, "radio_vad_enabled", "0") == 0);
|
||||
auto* manager = SoundManager::instance();
|
||||
REQUIRE(manager->init());
|
||||
QTemporaryDir directory;
|
||||
REQUIRE(directory.isValid());
|
||||
QString configPath = directory.filePath("radio.cfg");
|
||||
QFile config(configPath);
|
||||
REQUIRE(config.open(QIODevice::WriteOnly));
|
||||
REQUIRE(config.write("[chatserver]\nradio_vad_enabled=0\nradio_opus_bitrate=32\n") > 0);
|
||||
config.close();
|
||||
auto* engine = RadioAudioEngine::instance();
|
||||
engine->loadSettings(configPath);
|
||||
await([&]() { return engine->settingsReady() && !engine->settingsBusy(); });
|
||||
REQUIRE(!engine->vadEnabled() && engine->setting("radio_vad_compressor_max_gain_db") == 15);
|
||||
RadioPanel panel;
|
||||
panel.resize(580, 50);
|
||||
panel.show();
|
||||
auto* vad = panel.findChild<QPushButton*>("radioVadButton");
|
||||
auto* ptt = panel.findChild<QPushButton*>("radioPttButton");
|
||||
auto* status = panel.findChild<QLabel*>("radioStatus");
|
||||
REQUIRE(vad && ptt && status && !vad->isEnabled());
|
||||
REQUIRE(engine->start(7) == 0);
|
||||
await([&]() { return engine->captureAvailable() && ptt->isEnabled(); });
|
||||
REQUIRE(!vad->isChecked() && vad->isEnabled());
|
||||
REQUIRE(vad->geometry().right() < ptt->geometry().left());
|
||||
int oldOutputs = outputs, oldInputs = inputs;
|
||||
vad->click();
|
||||
REQUIRE(engine->settingsBusy() && !vad->isEnabled());
|
||||
await([&]() { return !engine->settingsBusy() && engine->vadAvailable(); });
|
||||
REQUIRE(vad->isChecked() && outputs == oldOutputs && inputs == oldInputs + 1);
|
||||
pump(750); // Полное кольцо prefetch до начала речи.
|
||||
int previousPackets = packets;
|
||||
probability = 0.9f;
|
||||
await([&]() { return engine->isTransmitting(); });
|
||||
REQUIRE(!engine->isTalking() && ptt->property("transmitting").toBool() && begins == 1);
|
||||
REQUIRE(packets >= previousPackets + 30);
|
||||
int beforeManual = begins;
|
||||
QTest::mousePress(ptt, Qt::LeftButton);
|
||||
REQUIRE(engine->isTalking());
|
||||
engine->setHotkeyTalking(true);
|
||||
QTest::mouseRelease(ptt, Qt::LeftButton);
|
||||
REQUIRE(engine->isTalking());
|
||||
engine->setHotkeyTalking(false);
|
||||
pump(100);
|
||||
REQUIRE(!engine->isTalking() && engine->isTransmitting() && begins == beforeManual);
|
||||
probability = 0;
|
||||
await([&]() { return !engine->isTransmitting(); });
|
||||
REQUIRE(!ptt->property("transmitting").toBool() && ends == 1);
|
||||
qInfo("radio panel: automatic TX, 600ms prefetch, actual UI state and manual/hotkey handover PASS");
|
||||
|
||||
engine->setHotkeyTalking(true);
|
||||
await([&]() { return engine->isTransmitting(); });
|
||||
oldOutputs = outputs; oldInputs = inputs;
|
||||
vad->click();
|
||||
await([&]() { return !engine->settingsBusy() && !engine->vadEnabled() && engine->isTransmitting(); });
|
||||
REQUIRE(engine->isTalking() && !engine->vadAvailable() && outputs == oldOutputs && inputs == oldInputs + 1);
|
||||
engine->setHotkeyTalking(false);
|
||||
await([&]() { return !engine->isTransmitting(); });
|
||||
REQUIRE(engine->applySettings({{"radio_vad_threshold", 65}, {"radio_vad_hangover_ms", 400},
|
||||
{"radio_vad_compressor_max_gain_db", 30}, {"radio_compressor_max_gain_db", 6}}));
|
||||
await([&]() { return !engine->settingsBusy(); });
|
||||
REQUIRE(engine->setting("radio_vad_compressor_max_gain_db") == 30 && engine->setting("radio_compressor_max_gain_db") == 6);
|
||||
REQUIRE(!engine->applySettings({{"radio_vad_compressor_max_gain_db", 61}}));
|
||||
REQUIRE(engine->setting("radio_vad_compressor_max_gain_db") == 30);
|
||||
|
||||
detectorAvailable = false;
|
||||
vad->click();
|
||||
await([&]() { return !engine->settingsBusy(); });
|
||||
REQUIRE(vad->isChecked() && engine->captureAvailable() && !engine->vadAvailable() && status->text() == "VAD недоступен");
|
||||
QTest::mousePress(ptt, Qt::LeftButton);
|
||||
await([&]() { return engine->isTransmitting(); });
|
||||
QTest::mouseRelease(ptt, Qt::LeftButton);
|
||||
await([&]() { return !engine->isTransmitting(); });
|
||||
qInfo("radio panel: capture-only restart preserves output/manual owner; VAD failure leaves manual PTT working PASS");
|
||||
|
||||
engine->stop();
|
||||
REQUIRE(!engine->isTransmitting() && !engine->captureAvailable() && !vad->isEnabled());
|
||||
REQUIRE(config.open(QIODevice::ReadOnly));
|
||||
QByteArray saved = config.readAll();
|
||||
config.close();
|
||||
REQUIRE(saved.contains("radio_vad_enabled=1") && saved.contains("radio_vad_threshold=65") &&
|
||||
saved.contains("radio_vad_hangover_ms=400") && saved.contains("radio_vad_compressor_max_gain_db=30") &&
|
||||
saved.contains("radio_opus_bitrate=32"));
|
||||
engine->loadSettings(configPath);
|
||||
await([&]() { return !engine->settingsBusy(); });
|
||||
REQUIRE(engine->vadEnabled() && engine->setting("radio_vad_threshold") == 65);
|
||||
detectorAvailable = true;
|
||||
REQUIRE(engine->start(8) == 0);
|
||||
await([&]() { return engine->vadAvailable(); });
|
||||
REQUIRE(engine->groupId() == 8 && vad->isChecked());
|
||||
engine->stop();
|
||||
engine->loadSettings(directory.filePath("missing.cfg"));
|
||||
await([&]() { return !engine->settingsBusy(); });
|
||||
REQUIRE(!engine->applySettings({{"radio_vad_enabled", 0}}) && engine->vadEnabled() && !engine->settingsError().isEmpty());
|
||||
for (auto* button : panel.findChildren<QToolButton*>()) {
|
||||
REQUIRE(QMetaObject::invokeMethod(button->menu(), "aboutToShow", Qt::DirectConnection));
|
||||
REQUIRE(!button->menu()->actions().isEmpty());
|
||||
}
|
||||
panel.grab().save("/tmp/desktop-radio-panel.png");
|
||||
radio_audio_destroy(&instance);
|
||||
uasync_destroy(instance.ua, 0);
|
||||
instance.ua = nullptr;
|
||||
manager->shutdown();
|
||||
qInfo("radio panel: saved settings, channel change, device menus, range validation and config failure PASS");
|
||||
return 0;
|
||||
}
|
||||
Reference in new issue
Block a user