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.
292 lines
15 KiB
292 lines
15 KiB
/* Настоящие 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 <QCoreApplication> |
|
#include <QThread> |
|
#include <QFile> |
|
#include <cstring> |
|
#include <algorithm> |
|
#include <map> |
|
#include <vector> |
|
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<ma_context*, int> live; |
|
static std::vector<ma_device*> 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" 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<int16_t> 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<int16_t> 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<AudioDevice*>(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<int16_t> 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<int16_t> 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; |
|
}
|
|
|