/* Настоящие null devices и Qt timers; linker wrappers инъецируют только сбои lifecycle. */ #include "../src/audio_device.h" #include "../src/call_audio_engine.h" #include "../src/radio_audio_engine.h" #include "../src/audiorecorder.h" #include "../src/sound_manager.h" #include #include #include #include #include #include #include extern "C" { #include "utun_instance.h" #include "call/call_audio.h" #include "radio/radio_audio.h" #include "media_async/attachment_send.h" #include "u_async.h" #include "debug_config.h" } #define REQUIRE(condition) do { if (!(condition)) qFatal("audio recovery line %d: %s", __LINE__, #condition); } while (0) static UTUN_INSTANCE instance{}; static std::map live; static std::vector devices; static int starts, captureStarts, playbackStarts, contextStarts, failContexts; static bool failStart, failContext, failDevice; static bool pulseIntegration; 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" ma_result __real_ma_context_init(const ma_backend*, ma_uint32, const ma_context_config*, ma_context*); extern "C" void __real_ma_context_uninit(ma_context*); extern "C" ma_result __real_ma_device_init(ma_context*, const ma_device_config*, ma_device*); 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" ma_result __wrap_ma_context_init(const ma_backend*, ma_uint32, const ma_context_config*, ma_context* context) { ++contextStarts; if (failContexts > 0) { --failContexts; return MA_ERROR; } ma_backend backend = pulseIntegration ? ma_backend_pulseaudio : ma_backend_null; return __real_ma_context_init(&backend, 1, nullptr, context); } extern "C" void __wrap_ma_context_uninit(ma_context* context) { REQUIRE(live[context] == 0); __real_ma_context_uninit(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); if (result == MA_SUCCESS) { ++live[context]; devices.push_back(device); } return result; } extern "C" ma_result __wrap_ma_device_start(ma_device* device) { ++starts; if (device->type == ma_device_type_capture) { ++captureStarts; // Некоторые backends вызывают data callback ещё до возврата start. std::vector input(1200 * device->capture.channels, 1000); if (!pulseIntegration) device->onData(device, nullptr, input.data(), 1200); } else ++playbackStarts; if (failStart) { failStart = false; return MA_ERROR; } return __real_ma_device_start(device); } extern "C" void __wrap_ma_device_uninit(ma_device* device) { auto context = device->pContext; __real_ma_device_uninit(device); REQUIRE(live[context] > 0); --live[context]; devices.erase(std::find(devices.begin(), devices.end(), device)); } extern "C" int __wrap_audio_context_failed(ma_context* context) { if (failContext) { failContext = false; return 1; } return __real_audio_context_failed(context); } static void pump(int milliseconds) { QElapsedTimer timer; timer.start(); while (timer.elapsed() < milliseconds) { QCoreApplication::processEvents(); 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(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); QString separate; for (const auto& output : manager->enumeratePlaybackDevices()) { ma_device_id id{}; if (manager->deviceId(output.id, &id) && !strcmp(id.pulse, "utun_test_voice")) separate = output.id; } REQUIRE(!separate.isEmpty()); call->setPlaybackDevice(separate); AudioRecorder recorder; recorder.init(); recorder.startRecording(); std::vector pcm(48000 * 20, 1000); auto pcmId = manager->playRawPcm(pcm.data(), (int)pcm.size(), 48000, 1); for (int i = 0; i < 100 && recorder.durationMs() < 300; ++i) pump(50); REQUIRE(recorder.durationMs() >= 300 && manager->pcmCursor(pcmId) > 0); QFile routesReady(control + "/routes_ready"); REQUIRE(routesReady.open(QIODevice::WriteOnly)); routesReady.close(); for (int i = 0; i < 100 && !QFile::exists(control + "/moved"); ++i) pump(50); REQUIRE(QFile::exists(control + "/moved")); pump(500); int outputs = 0; for (auto* device : devices) if (device->type == ma_device_type_playback) ++outputs; REQUIRE(outputs == 1); // Сервер переместил default stream на voice sink: получился один микшер. qInfo("PulseAudio route move: call/radio/media merged into one output PASS"); for (auto* device : devices) if (device->type == ma_device_type_playback) REQUIRE(ma_device_stop(device) == MA_SUCCESS); pump(1000); outputs = 0; uint64_t wantedRoute = UINT64_C(14695981039346656037); for (const char* name = "utun_test_voice"; *name; ++name) { wantedRoute ^= (unsigned char)*name; wantedRoute *= UINT64_C(1099511628211); } bool foundWanted = false; for (auto* device : devices) if (device->type == ma_device_type_playback) { ++outputs; if (static_cast(device->pUserData)->route() == wantedRoute) foundWanted = true; } REQUIRE(foundWanted && outputs >= 1 && outputs <= 2); // Сервер может восстановить прежний move для default stream. qInfo("PulseAudio output recovery: desired call route preserved outputs=%d PASS", outputs); int duration = recorder.durationMs(); auto cursor = manager->pcmCursor(pcmId); QFile ready(control + "/ready"); REQUIRE(ready.open(QIODevice::WriteOnly)); ready.close(); for (int i = 0; i < 100 && manager->context(); ++i) pump(50); REQUIRE(!manager->context()); QFile closed(control + "/closed"); REQUIRE(closed.open(QIODevice::WriteOnly)); closed.close(); for (int i = 0; i < 150 && (!manager->context() || recorder.durationMs() <= duration + 200); ++i) pump(50); REQUIRE(manager->context() && recorder.durationMs() > duration + 200 && manager->pcmCursor(pcmId) > cursor); REQUIRE(call->isActive() && call->callId() == 42 && radio->isActive() && radio->groupId() == 7); 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; manager->shutdown(); REQUIRE(devices.empty()); qInfo("real PulseAudio: capture/playback progress restored after server restart PASS"); } int main(int argc, char** argv) { QCoreApplication app(argc, argv); pulseIntegration = argc == 3 && std::strcmp(argv[1], "--pulse-integration") == 0; debug_config_init(); debug_set_level(pulseIntegration ? DEBUG_LEVEL_INFO : DEBUG_LEVEL_WARN); auto* manager = SoundManager::instance(); REQUIRE(manager->init() && manager->context()); if (pulseIntegration) { pulse_recovery(manager, QString::fromLocal8Bit(argv[2])); return 0; } ma_device_id id{}; auto outputs = manager->enumeratePlaybackDevices(); REQUIRE(!outputs.empty() && SoundManager::deviceId(outputs.front().id, &id)); REQUIRE(!SoundManager::deviceId(outputs.front().id + '!', &id)); { AudioDevice device; auto config = ma_device_config_init(ma_device_type_capture); config.capture.format = ma_format_s16; config.capture.channels = 1; config.sampleRate = 48000; REQUIRE(device.init(manager->context(), config, "test-partial-start", 0, [](const void*, void*, unsigned int) {})); int count = live[manager->context()]; failStart = true; REQUIRE(!device.start() && !device.initialized()); REQUIRE(live[manager->context()] == count - 1); failDevice = true; REQUIRE(!device.init(manager->context(), config, "test-init-failure", 0, [](const void*, void*, unsigned int) {})); REQUIRE(!device.initialized() && live[manager->context()] == count - 1); } { AudioRecorder first, second; first.init(); second.init(); auto inputs = manager->enumerateCaptureDevices(); REQUIRE(!inputs.empty()); first.setCaptureDevice(inputs.front().id); int previous = captureStarts; failStart = true; first.startRecording(); second.startRecording(); REQUIRE(captureStarts == previous + 3); // Выбранный input не стартовал: default запущен после полного uninit. pump(60); first.stopRecording(); second.stopRecording(); attachment_send_req a{}, b{}; REQUIRE(first.takeRecording(&a) && second.takeRecording(&b)); REQUIRE(a.pcm_count >= 1200 && b.pcm_count >= 1200); REQUIRE(a.pcm[0] == 1000 && b.pcm[0] == 1000); // pUserData маршрутизирует каждый callback своему recorder. a.pcm_release(a.pcm_owner); b.pcm_release(b.pcm_owner); } { AudioRecorder meter; meter.init(); meter.startRecording(true); pump(60); meter.stopRecording(); attachment_send_req request{}; REQUIRE(meter.durationMs() > 0 && !meter.takeRecording(&request)); REQUIRE(!request.pcm && !request.pcm_owner && !request.compressor); } instance.ua = uasync_create(); REQUIRE(instance.ua); REQUIRE(call_audio_init(&instance) == 0 && radio_audio_init(&instance) == 0); auto* call = CallAudioEngine::instance(); REQUIRE(call->start(42) == 0); pump(60); ma_device* playback = nullptr; for (auto* device : devices) if (device->type == ma_device_type_playback && device->playback.format == ma_format_f32 && device->playback.channels == 2) playback = device; REQUIRE(playback); int previousCapture = captureStarts, previousPlayback = playbackStarts; REQUIRE(ma_device_stop(playback) == MA_SUCCESS); pump(800); REQUIRE(captureStarts == previousCapture && playbackStarts > previousPlayback); REQUIRE(call->callId() == 42 && call->isActive()); auto* radio = RadioAudioEngine::instance(); REQUIRE(radio->start(7) == 0); AudioRecorder recorder; recorder.init(); recorder.startRecording(); std::vector notification(48000 * 10, 1000); auto pcmId = manager->playRawPcm(notification.data(), (int)notification.size(), 48000, 1); pump(60); auto cursor = manager->pcmCursor(pcmId); REQUIRE(cursor > 0); int previousContext = contextStarts; failContext = true; failContexts = 2; pump(1800); REQUIRE(contextStarts >= previousContext + 3 && manager->context()); REQUIRE(call->callId() == 42 && radio->groupId() == 7 && recorder.isRecording()); REQUIRE(manager->pcmCursor(pcmId) > cursor && manager->isPcmPlaying(pcmId)); call->stop(); radio->stop(); recorder.stopRecording(); int previousStarts = starts; pump(600); REQUIRE(starts == previousStarts); // Отложенное recovery не воскрешает остановленную сессию. REQUIRE(call->start(43) == 0); call->beginEnd(43); REQUIRE(call->start(43) == 0); pump(600); REQUIRE(call->isActive() && call->callId() == 43); // Старый end timer не закрывает новое поколение того же call ID. call->stop(); failContext = true; failContexts = 1; recorder.startRecording(); pump(220); REQUIRE(!manager->context()); recorder.stopRecording(); previousCapture = captureStarts; pump(1000); REQUIRE(manager->context() && captureStarts == previousCapture); // Частота источника и управление сессией независимы от соседнего проигрывателя. std::vector pcm24(24000 * 2, 2000), pcm48(48000 * 2, -1000); auto player24 = manager->playRawPcm(pcm24.data(), 48000, 24000, 1); auto player48 = manager->playRawPcm(pcm48.data(), 96000, 48000, 1); REQUIRE(player24 && player48 && player24 != player48); pump(150); uint64_t cursor24 = manager->pcmCursor(player24), cursor48 = manager->pcmCursor(player48); REQUIRE(cursor24 > 1000 && cursor24 < 6000 && cursor48 > cursor24 * 1.8 && cursor48 < cursor24 * 2.2); manager->pausePcm(player24); cursor24 = manager->pcmCursor(player24); pump(80); uint64_t pausedAfter = manager->pcmCursor(player24), neighbourAfter = manager->pcmCursor(player48); qInfo("PCM pause: paused=%llu->%llu neighbour=%llu->%llu", (unsigned long long)cursor24, (unsigned long long)pausedAfter, (unsigned long long)cursor48, (unsigned long long)neighbourAfter); REQUIRE(pausedAfter == cursor24 && neighbourAfter > cursor48); REQUIRE(manager->seekPcm(player24, 12000)); REQUIRE(manager->pcmCursor(player24) == 12000 && !manager->isPcmPlaying(player24)); manager->resumePcm(player24); // Пауза синхронизируется с уже начатым чтением графа, сосед продолжает играть. for (int i = 0; i < 50; ++i) { pump(1); manager->pausePcm(player24); uint64_t paused = manager->pcmCursor(player24); pump(1); REQUIRE(manager->pcmCursor(player24) == paused); manager->resumePcm(player24); } manager->stopPcm(player48); pump(60); REQUIRE(manager->pcmCursor(player24) > 12000 && manager->pcmCursor(player48) == 0); manager->stopPcm(player24); // Предел TX действует до помещения задачи в uasync; старое поколение не отправляется после stop/start. uasync_poll(instance.ua, 0); REQUIRE(call_audio_start(99) == 0); int16_t silence[960]{}; for (int i = 0; i < 100; ++i) call_audio_feed_pcm(99, silence, 960); int pending = 0; for (auto* task = instance.ua->posted_tasks_head; task; task = task->next) ++pending; REQUIRE(pending == 8); call_audio_stop(); REQUIRE(call_audio_start(100) == 0); for (int i = 0; i < 100; ++i) call_audio_feed_pcm(100, silence, 960); pending = 0; for (auto* task = instance.ua->posted_tasks_head; task; task = task->next) ++pending; REQUIRE(pending == 8); // Новый звонок также учитывает ещё не освобождённые задачи старого. call_audio_stop(); uasync_poll(instance.ua, 0); REQUIRE(!instance.ua->posted_tasks_head); REQUIRE(radio_audio_start(8, 1) == 0); 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); uasync_destroy(instance.ua, 0); instance.ua = nullptr; manager->shutdown(); REQUIRE(devices.empty()); qInfo("audio recovery: partial start, independent streams, two recorders, context restart, cancellation, bounded TX PASS"); return 0; }