|
|
|
|
@ -1,4 +1,4 @@
|
|
|
|
|
/* Настоящие null devices и Qt timers; linker wrappers инъецируют только сбои lifecycle. */ |
|
|
|
|
/* Настоящие null/PulseAudio devices и Qt timers; wrappers инъецируют сбои либо параметры ручного замера. */ |
|
|
|
|
#include "../src/audio_device.h" |
|
|
|
|
#include "../src/call_audio_engine.h" |
|
|
|
|
#include "../src/radio_audio_engine.h" |
|
|
|
|
@ -15,6 +15,7 @@ extern "C" {
|
|
|
|
|
#include "utun_instance.h" |
|
|
|
|
#include "call/call_audio.h" |
|
|
|
|
#include "radio/radio_audio.h" |
|
|
|
|
#include "radio/radio.h" |
|
|
|
|
#include "media_async/attachment_send.h" |
|
|
|
|
#include "u_async.h" |
|
|
|
|
#include "debug_config.h" |
|
|
|
|
@ -27,6 +28,8 @@ static std::vector<ma_device*> devices;
|
|
|
|
|
static int starts, captureStarts, playbackStarts, contextStarts, failContexts; |
|
|
|
|
static bool failStart, failContext, failDevice; |
|
|
|
|
static bool pulseIntegration; |
|
|
|
|
static bool latencyProbe; |
|
|
|
|
static unsigned probeCaptureFrames, probePlaybackFrames; |
|
|
|
|
|
|
|
|
|
extern "C" UTUN_INSTANCE* gui_bridge_get_inst() { return &instance; } |
|
|
|
|
extern "C" int gui_bridge_is_uasync_ready() { return instance.ua != nullptr; } |
|
|
|
|
@ -37,6 +40,10 @@ extern "C" ma_result __real_ma_device_init(ma_context*, const ma_device_config*,
|
|
|
|
|
extern "C" ma_result __real_ma_device_start(ma_device*); |
|
|
|
|
extern "C" void __real_ma_device_uninit(ma_device*); |
|
|
|
|
extern "C" int __real_audio_context_failed(ma_context*); |
|
|
|
|
extern "C" int __real_call_send_media(UTUN_INSTANCE*, uint64_t, const uint8_t*, int, const call_capture_time*); |
|
|
|
|
extern "C" int __wrap_call_send_media(UTUN_INSTANCE* inst, uint64_t id, const uint8_t* data, int size, const call_capture_time* timing) { |
|
|
|
|
return latencyProbe ? 0 : __real_call_send_media(inst, id, data, size, timing); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
extern "C" ma_result __wrap_ma_context_init(const ma_backend*, ma_uint32, const ma_context_config*, ma_context* context) { |
|
|
|
|
++contextStarts; |
|
|
|
|
@ -50,7 +57,14 @@ 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_result result = __real_ma_device_init(context, config, device); |
|
|
|
|
ma_device_config requested = *config; |
|
|
|
|
if (latencyProbe) { |
|
|
|
|
requested.periodSizeInMilliseconds = 0; |
|
|
|
|
requested.periodSizeInFrames = config->deviceType == ma_device_type_capture ? probeCaptureFrames : probePlaybackFrames; |
|
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_CALL, "latency-probe: override type=%d period_frames=%u rate=%u", |
|
|
|
|
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); } |
|
|
|
|
return result; |
|
|
|
|
} |
|
|
|
|
@ -79,12 +93,17 @@ extern "C" int __wrap_audio_context_failed(ma_context* context) {
|
|
|
|
|
|
|
|
|
|
static void pump(int milliseconds) { |
|
|
|
|
QElapsedTimer timer; timer.start(); |
|
|
|
|
while (timer.elapsed() < milliseconds) { QCoreApplication::processEvents(); QThread::msleep(1); } |
|
|
|
|
while (timer.elapsed() < milliseconds) { |
|
|
|
|
QCoreApplication::processEvents(); |
|
|
|
|
if (latencyProbe && instance.ua) uasync_poll(instance.ua, 0); |
|
|
|
|
QThread::msleep(1); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
static void pulse_recovery(SoundManager* manager, const QString& control) { |
|
|
|
|
REQUIRE(manager->context()->backend == ma_backend_pulseaudio); |
|
|
|
|
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(42) == 0 && radio->start(7) == 0); |
|
|
|
|
@ -130,18 +149,58 @@ static void pulse_recovery(SoundManager* manager, const QString& control) {
|
|
|
|
|
qInfo("PulseAudio progress: capture=%d -> %dms playback=%llu -> %llu frames", duration, recorder.durationMs(), |
|
|
|
|
(unsigned long long)cursor, (unsigned long long)manager->pcmCursor(pcmId)); |
|
|
|
|
call->stop(); radio->stop(); recorder.stopRecording(); |
|
|
|
|
call_audio_destroy(&instance); radio_audio_destroy(&instance); uasync_destroy(instance.ua, 0); instance.ua = nullptr; |
|
|
|
|
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("real PulseAudio: capture/playback progress restored after server restart PASS"); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* Ручной стенд: настоящий I/O и voice workers, без сети и сохранения микрофонного PCM.
|
|
|
|
|
* --latency capture_frames playback_frames seconds [call|radio|output] [log]; 0 означает backend default. */ |
|
|
|
|
static void measure_latency(SoundManager* manager, int seconds, const QString& kind) { |
|
|
|
|
REQUIRE(manager->context()->backend == ma_backend_pulseaudio); |
|
|
|
|
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(); |
|
|
|
|
if (kind == "call") REQUIRE(call->start(42) == 0); |
|
|
|
|
if (kind == "radio") REQUIRE(radio->start(7) == 0); |
|
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_CALL, "latency-probe: begin kind=%s seconds=%d capture_frames=%u playback_frames=%u", |
|
|
|
|
qPrintable(kind), seconds, probeCaptureFrames, probePlaybackFrames); |
|
|
|
|
pump(seconds * 1000); |
|
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_CALL, "latency-probe: end kind=%s", qPrintable(kind)); |
|
|
|
|
call->stop(); radio->stop(); |
|
|
|
|
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()); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
int main(int argc, char** argv) { |
|
|
|
|
QCoreApplication app(argc, argv); |
|
|
|
|
pulseIntegration = argc == 3 && std::strcmp(argv[1], "--pulse-integration") == 0; |
|
|
|
|
latencyProbe = argc >= 2 && std::strcmp(argv[1], "--latency") == 0; |
|
|
|
|
int seconds = 0; |
|
|
|
|
QString kind = argc >= 6 ? QString::fromLocal8Bit(argv[5]) : "call"; |
|
|
|
|
if (latencyProbe) { |
|
|
|
|
REQUIRE(argc >= 5 && argc <= 7); |
|
|
|
|
bool captureOk, playbackOk, durationOk; |
|
|
|
|
probeCaptureFrames = QString::fromLocal8Bit(argv[2]).toUInt(&captureOk); |
|
|
|
|
probePlaybackFrames = QString::fromLocal8Bit(argv[3]).toUInt(&playbackOk); |
|
|
|
|
seconds = QString::fromLocal8Bit(argv[4]).toInt(&durationOk); |
|
|
|
|
REQUIRE(captureOk && playbackOk && durationOk && seconds > 0 && seconds <= 3600); |
|
|
|
|
REQUIRE(probeCaptureFrames <= 4800 && probePlaybackFrames <= 4800); |
|
|
|
|
REQUIRE(kind == "call" || kind == "radio" || kind == "output"); |
|
|
|
|
} |
|
|
|
|
pulseIntegration = latencyProbe || (argc == 3 && std::strcmp(argv[1], "--pulse-integration") == 0); |
|
|
|
|
debug_config_init(); debug_set_level(pulseIntegration ? DEBUG_LEVEL_INFO : DEBUG_LEVEL_WARN); |
|
|
|
|
if (latencyProbe) { |
|
|
|
|
debug_set_category_level(DEBUG_CATEGORY_CALL, DEBUG_LEVEL_TRACE); |
|
|
|
|
debug_set_category_level(DEBUG_CATEGORY_AEC, DEBUG_LEVEL_DEBUG); |
|
|
|
|
if (argc == 7) { debug_enable_file_output(argv[6], 1); REQUIRE(g_debug_config.file_output); } |
|
|
|
|
} |
|
|
|
|
auto* manager = SoundManager::instance(); |
|
|
|
|
REQUIRE(manager->init() && manager->context()); |
|
|
|
|
if (latencyProbe) { measure_latency(manager, seconds, kind); return 0; } |
|
|
|
|
if (pulseIntegration) { pulse_recovery(manager, QString::fromLocal8Bit(argv[2])); return 0; } |
|
|
|
|
ma_device_id id{}; |
|
|
|
|
auto outputs = manager->enumeratePlaybackDevices(); |
|
|
|
|
@ -185,6 +244,7 @@ int main(int argc, char** argv) {
|
|
|
|
|
REQUIRE(!request.pcm && !request.pcm_owner && !request.compressor); |
|
|
|
|
} |
|
|
|
|
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(); |
|
|
|
|
REQUIRE(call->start(42) == 0); |
|
|
|
|
@ -285,7 +345,7 @@ int main(int argc, char** argv) {
|
|
|
|
|
radio_audio_talk_begin(8); radio_audio_talk_end(8); radio_audio_stop(); |
|
|
|
|
REQUIRE(instance.ua->posted_tasks_head); |
|
|
|
|
// Завершение ядра освобождает ещё не выполненные PTT-задачи вместе с аргументами.
|
|
|
|
|
call_audio_destroy(&instance); radio_audio_destroy(&instance); |
|
|
|
|
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()); |
|
|
|
|
|