|
|
|
|
@ -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{}; |
|
|
|
|
|