You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
180 lines
8.7 KiB
180 lines
8.7 KiB
/* Настоящие 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; |
|
}
|
|
|