Сохрани накопленные исправления аудио, async и диагностики
This commit is contained in:
1 parent
b1e3193899
commit
bdcf8b5634
59 files changed
+4611
-1441
No files matched your search
@@ -0,0 +1,222 @@
|
||||
/* Настоящие 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);
|
||||
AudioRecorder recorder; recorder.init(); recorder.startRecording();
|
||||
std::vector<int16_t> pcm(48000 * 20, 1000);
|
||||
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->currentPcmCursor() > 0);
|
||||
int duration = recorder.durationMs();
|
||||
auto cursor = manager->currentPcmCursor();
|
||||
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->currentPcmCursor() > cursor);
|
||||
qInfo("PulseAudio progress: capture=%d -> %dms playback=%llu -> %llu frames", duration, recorder.durationMs(),
|
||||
(unsigned long long)cursor, manager->currentPcmCursor());
|
||||
recorder.stopRecording(); 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);
|
||||
}
|
||||
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_s16 && device->playback.channels == 1)
|
||||
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);
|
||||
manager->playRawPcm(notification.data(), (int)notification.size(), 48000, 1);
|
||||
pump(60);
|
||||
auto cursor = manager->currentPcmCursor();
|
||||
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->currentPcmCursor() > cursor && manager->isCurrentPcmPlaying());
|
||||
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);
|
||||
|
||||
// Предел 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;
|
||||
}
|
||||
Reference in new issue
Block a user